Compare commits

...

7 Commits

  1. 4
      .phan/config.php
  2. 152
      README.md
  3. 18
      TODO.md
  4. 18
      app/LayoutDriver/Xrandr.php
  5. 93
      app/System/Command.php
  6. 4
      app/System/Command/Xrandr.php
  7. 25
      app/System/CommandArgument.php
  8. 585
      app/System/Process.php
  9. 18
      app/System/RawCommandArgument.php
  10. 13
      app/System/SensitiveCommandArgument.php
  11. 8
      composer.json
  12. 45
      composer.lock

4
.phan/config.php

@ -12,11 +12,13 @@ return [
'vendor/laravel-zero/',
'vendor/nunomaduro/',
'vendor/composer/',
'vendor/symfony/'
'vendor/symfony/',
'vendor/nucleardog/'
],
'exclude_analysis_directory_list' => [
'vendor/',
'app/System/',
'.phan/stubs/'
],

152
README.md

@ -0,0 +1,152 @@
# monitor\_layout
A tool for specifying monitor layout on Linux.
## Installation
### Pre-requisites
* PHP 7.2+
### Setup
* Extract the tool.
## Usage
### layout
monitor_layout layout <config_file.yaml>
Will load `config_file.yaml` and attempt to lay out all attached screens
following the layout rules.
### monitor
monitor_layout monitor <config_file.yaml> [--interval=5]
Will load `config_file.yaml` and attempt to lay out all attached screens
following the layout rules.
The program will stay running and attempt to perform layout again every
`interval` seconds in order to respond to screens being attached/unattached.
## Configuration
### Screens
```
screens:
internal:
output: eDP-1-1
hdmi:
output:
- HDMI-1-1
- HDMI-1-2
usb:
output: DVI-I-2-1
```
Each screen provides a single logical resource for dealing with your display in
various configurations. The program will enumerate the outputs specified until
it finds one that is connected, and that screen will refer to that output. This
allows for, example, allowing connecting a display to *any* of a variety of
outputs and having it treated as a single logical screen.
### Layouts
```
layouts:
all_screens:
screens:
internal: null
hdmi: null
usb:
width: 1920
height: 1080
links:
- display: hdmi
right_of: internal
- display: usb
above: hdmi
hdmi_only:
screens:
internal: null
hdmi: null
links:
- display: hdmi
right_of: internal
```
Layouts are evaluated in the order listed. The first layout found where all the
listed `screens` are available (that is, xrandr shows them as connected) will be
used.
The selected layout can specify additional parameters (e.g., width and height)
for the screen.
The `links` will be used to calculate the offsets of all the displays relative
to whichever screen is considered `primary` by xrandr. Valid relations are:
* `above` - above the referenced display, centered horizontally
* `below` - below the referenced display, centered horizontally
* `left_of` - to the left of the referenced display, with the tops aligned
* `right_of` - to the right of the referenced display, with the tops aligned
### Sample
```
screens:
hdmi:
output: HDMI-1-2
touchpad:
output: HDMI-1-1
internal:
output: eDP-1-1
usb:
output:
- DVI-I-2-1
- DVI-I-2-2
- DVI-I-2-3
- DVI-I-2-4
- DVI-I-2-5
- DVI-I-2-6
layouts:
all_screens:
screens:
hdmi: null
touchpad: null
internal: null
usb:
width: 1920
height: 1080
links:
- display: usb
right_of: internal
- display: hdmi
above: usb
- display: touchpad
below: internal
no_usb:
screens:
hdmi: null
touchpad: null
internal: null
links:
- display: hdmi
right_of: internal
- display: touchpad
below: internal
```
## Building
`php monitor_layout app:build`
* This will occassionally fail due to the system's limit on open file
descriptors. The most straightforward (if slightly gross) solution is
simply: `sudo -- sh -c 'ulimit -n 65535 && ./monitor_layout app:build'`

18
TODO.md

@ -0,0 +1,18 @@
* Handle using xrandr to --auto/--off displays as appropriate.
* Provide some sort of eventing mechanism to update i3 configs as well.
* Only allow an output to be considered as one screen to allow, e.g.,
```
screens:
internal:
output: eDP-1-1
hdmi_primary:
output:
- HDMI-1-1
- HDMI-1-2
hdmi_secondary:
output:
- HDMI-1-2
- HDMI-1-1
```
This would allow creating a layout for "one monitor connected to either port"
and one for "both ports connected".

18
app/LayoutDriver/Xrandr.php

@ -5,26 +5,26 @@ declare(strict_types=1);
namespace App\LayoutDriver;
/**
* Interface to xrandr for configuring outputs
* Interface to xrandr for configuring outputs.
*
* @author Adam Pippin <hello@adampippin.ca>
*/
class Xrandr implements \App\ILayoutDriver
{
/**
* The xrandr command
* The xrandr command.
* @var \App\System\Command\Xrandr
*/
protected $_xrandr;
/**
* Cache of current display parameters
* Cache of current display parameters.
* @var array<string, array>
*/
protected $_displays;
/**
* Create a new xrandr driver
* Create a new xrandr driver.
*
* @param \App\System\Command\Xrandr $xrandr_cmd
*/
@ -34,7 +34,7 @@ class Xrandr implements \App\ILayoutDriver
}
/**
* Read display parameters from xrandr
* Read display parameters from xrandr.
*
* @return void
*/
@ -48,10 +48,10 @@ class Xrandr implements \App\ILayoutDriver
if (preg_match('/^(?<output>[^ ]+) connected (?<primary>primary )?(?<screen_w>[0-9]+)x(?<screen_h>[0-9]+)\\+(?<screen_x>[0-9]+)\\+(?<screen_y>[0-9]+) /', $line, $match))
{
$this->_displays[$match['output']] = [
'x' => $match['screen_x'],
'y' => $match['screen_y'],
'w' => $match['screen_w'],
'h' => $match['screen_h'],
'x' => (int)$match['screen_x'],
'y' => (int)$match['screen_y'],
'w' => (int)$match['screen_w'],
'h' => (int)$match['screen_h'],
'primary' => !empty($match['primary'])
];
}

93
app/System/Command.php

@ -1,93 +0,0 @@
<?php
declare(strict_types=1);
namespace App\System;
class Command
{
protected $command;
protected $defaultArguments;
protected $path;
public function __construct(string $command, array $defaultArguments = [])
{
$this->command = $command;
$this->defaultArguments = $defaultArguments;
$this->path = static::find($command);
if (!isset($this->path))
{
throw new \Exception('Command not found: '.$command);
}
}
public function getPath()
{
return $this->path;
}
public function create(array $arguments)
{
$arguments = array_merge($this->defaultArguments, $arguments);
foreach ($arguments as &$argument)
{
if (!is_object($argument) || !($argument instanceof CommandArgument))
{
$argument = new CommandArgument($argument);
}
}
$command = [];
if (strpos($this->path, ' ') === false)
{
$command[] = '"'.$this->path.'"';
}
else
{
$command[] = $this->path;
}
$command = array_merge($command, $arguments);
$process = app()->make('App\\System\\Process', ['command' => $command]);
$process->setStartModeSync();
return $process;
}
public function __invoke(array $arguments)
{
return $this->create($arguments)->start()->getStdout();
}
protected static function find($command)
{
if (PHP_OS == 'WINNT')
{
$result = shell_exec('where '.escapeshellarg($command));
// No idea why Windows is using unix line endings here
// Trim the trailing newline, last result is always empty
$result = explode("\n", trim($result));
$result = end($result);
}
else
{
$result = shell_exec('command -v '.escapeshellarg($command));
}
if (!is_string($result))
{
return;
}
$result = trim($result);
if (empty($result))
{
return;
}
return $result;
}
}

4
app/System/Command/Xrandr.php

@ -4,9 +4,7 @@ declare(strict_types=1);
namespace App\System\Command;
use App\System\Command;
class Xrandr extends Command
class Xrandr extends \Processes\Command
{
public function __construct()
{

25
app/System/CommandArgument.php

@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace App\System;
class CommandArgument
{
protected $value;
public function __construct($val)
{
$this->value = $val;
}
public function get()
{
return escapeshellarg($this->value);
}
public function __toString()
{
return escapeshellarg($this->value);
}
}

585
app/System/Process.php

@ -1,585 +0,0 @@
<?php
declare(strict_types=1);
namespace App\System;
class Process
{
/**
* The full command path to execute.
*
* This may include raw strings as well as instances of CommandArgument. When
* executing, the instances will be resolved with the CommandArgument::get()
* method. When printing for errors/logs/etc, it will instead be cast to string.
*
* @var array
*/
protected $command;
/**
* Whether to raise an exception if the process exits with a non-zero (failure)
* exit code.
*
* @var bool
*/
protected $raiseExceptionOnFailure = true;
/**
* An array of callbacks to call with each line of output.
*
* Each element should be an array with two elements. The first is a callable.
* The second is optional and may contain a preg-compatible regular expression.
* If this is set, then only lines matching this regular expression will trigger
* the callback and each invocation will include the matches array.
*
* @var array[][callable,string|null]
*/
protected $outputCallbacks = [];
/**
* An array of callbacks to call when the process is stopping.
*
* @var array[callable]
*/
protected $exitCallback = [];
/**
* How to start the process -- one of the START_MODE constants.
*
* @var int
*/
protected $startMode = self::START_MODE_ASYNC;
/**
* Start the process asynchronously. start() will return immediately.
*
* This uses PHP's tick functions to continue pumping the stdout/stderr
* streams and monitoring the process status.
*/
protected const START_MODE_ASYNC = 0x01;
/**
* Start the process sychronously. start() will return once the process
* exits.
*
* This uses stream_select to monitor for output on stdout/stderr and calls
* tick() to pump the streams and check the process status whenever that
* changes.
*/
protected const START_MODE_SYNC = 0x02;
/**
* Start the process bound to the parent process's streams.
*/
protected const START_MODE_REPLACE = 0x04;
/**
* Override the working directory of the spawned process.
*
* @var string|null
*/
protected $cwd;
/**
* Associative array of additional environment variables to set for the
* child process.
*
* @var array[string]
*/
protected $env = [];
/**
* The process's current status represented by one of the STATUS_ constants.
*
* @var int
*/
protected $status = self::STATUS_CREATED;
/**
* A new Process object has been created. The handle has not been opened.
*/
protected const STATUS_CREATED = 0x01;
/**
* The process is starting.
*/
protected const STATUS_STARTING = 0x02;
/**
* The process has been started and is now running.
*/
protected const STATUS_RUNNING = 0x04;
/**
* It looks like the process has exited. We'll call the exit callbacks.
*/
protected const STATUS_STOPPING = 0x05;
/**
* All exit callbacks have been completed.
*/
protected const STATUS_STOPPED = 0x06;
/**
* The process has exited, we've completed all our callbacks and
* unregistered the tick callback if applicable.
*/
protected const STATUS_EXITED = 0x07;
/**
* The process handle returned from proc_open.
* @var resource
*/
protected $handle;
/**
* The process's stdin stream.
*/
protected $stream_stdin;
/**
* The process's stdout stream.
*/
protected $stream_stdout;
/**
* The process's stderr stream.
*/
protected $stream_stderr;
/**
* All of the process's stdout output when not running in replace mode.
* @var string
*/
protected $stdout = null;
/**
* All of the process's stderr output when not running in replace mode.
* @var string
*/
protected $stderr = null;
/**
* When registering an output callback, bitwise constant to specify to
* listen to stdout output.
*
* @var int
*/
public const STREAM_STDOUT = 1;
/**
* When registering an output callback, bitwise constant to specify to
* listen to stderr output.
*
* @var int
*/
public const STREAM_STDERR = 2;
/**
* The process's exit status once it has exited.
* @var int|null
*/
protected $exitStatus = null;
public function __construct(array $command)
{
$this->command = $command;
}
/**
* Set an environment variable to pass to the child process.
*
* @param string $key
* @param string|null $value
* @return Process
*/
public function setEnv($key, $value)
{
if (isset($value))
{
$this->env[$key] = $value;
}
else
{
unset($this->env[$key]);
}
return $this;
}
/**
* Set multiple environment variables from an associative array.
*
* @param array $values
* @return Process
*/
public function setEnvRange(array $values)
{
foreach ($values as $k => $v)
{
$this->setEnv($k, $v);
}
return $this;
}
/**
* Whether to raise an exception if the process returns a non-zero exit
* code.
*
* @param bool $raiseException true to raise an exception
* @return void
*/
public function setRaiseExceptionOnFailure($raiseException)
{
$this->raiseExceptionOnFailure = $raiseException;
return $this;
}
/**
* Add an output callback to receive process stdout and stderr output.
*
* Signature is function(Process $process, string $line, array $matches = null)
*
* @param callable $callback the callback to call
* @param string $filter regex to use to filter lines before calling callback
* @param mixed $streams
* @return void
*/
public function addOutputCallback(callable $callback, $filter = null, $streams = Process::STREAM_STDOUT)
{
$this->outputCallbacks[] = [$callback, $filter, $streams];
return $this;
}
/**
* Add a callback to be called when the process transitions into stopping.
*
* @param callable $callback
* @return void
*/
public function addExitCallback(callable $callback)
{
$this->exitCallback[] = $callback;
return $this;
}
public function setStartModeSync()
{
$this->startMode = static::START_MODE_SYNC;
return $this;
}
public function setStartModeAsync()
{
$this->startMode = static::START_MODE_ASYNC;
return $this;
}
public function setStartModeReplace()
{
$this->startMode = static::START_MODE_SYNC | static::START_MODE_REPLACE;
return $this;
}
public function getStdout()
{
return $this->stdout;
}
public function getStdoutString()
{
return isset($this->stdout) ? implode(PHP_EOL, $this->stdout) : null;
}
public function getStderr()
{
return $this->stderr;
}
public function getStderrString()
{
return isset($this->stderr) ? implode(PHP_EOL, $this->stderr) : null;
}
public function getExitStatus()
{
return $this->exitStatus;
}
/**
* Check whether the process is still running. This will return false
* during the times it is transitioning to starting or into exiting.
*
* @return bool true if running
*/
public function isRunning()
{
return $this->status == static::STATUS_RUNNING;
}
public function fork()
{
if (PHP_OS == 'WINNT')
{
$command = sprintf(
'start /B %s',
$this->getCommand()
);
}
else
{
$command = sprintf(
'nohup %s > /dev/null 2>&1 &',
$this->getCommand()
);
}
shell_exec($command);
}
public function start()
{
$this->status = static::STATUS_STARTING;
if ($this->startMode & static::START_MODE_REPLACE)
{
$descriptors = [
0 => STDIN,
1 => STDOUT,
2 => STDERR
];
}
else
{
$descriptors = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w']
];
}
$pipes = null;
if (PHP_OS == 'WINNT')
{
$commandWrapper = '"';
}
else
{
$commandWrapper = '';
}
$this->handle = $handle = proc_open(
$commandWrapper.$this->getCommand().$commandWrapper,
$descriptors,
$pipes,
$this->cwd,
array_merge(getenv(), $this->env)
);
if (!is_resource($handle))
{
throw new \Exception('Failed to start process');
}
if (sizeof($pipes))
{
$this->stream_stdin = $pipes[0];
$this->stream_stdout = $pipes[1];
$this->stream_stderr = $pipes[2];
stream_set_blocking($pipes[0], false);
stream_set_blocking($pipes[1], false);
stream_set_blocking($pipes[2], false);
}
$this->status = static::STATUS_RUNNING;
if ($this->startMode & static::START_MODE_ASYNC)
{
declare(ticks=1);
register_tick_function([&$this, 'tick']);
}
elseif ($this->startMode & static::START_MODE_SYNC)
{
while ($this->status != static::STATUS_EXITED)
{
if ($this->startMode & static::START_MODE_REPLACE)
{
sleep(1);
}
else
{
$read_streams = [$pipes[1], $pipes[2]];
$write_streams = null;
$except_streams = null;
stream_select($read_streams, $write_streams, $except_streams, 1);
}
$this->tick();
}
}
return $this;
}
public function stop()
{
proc_terminate($this->handle);
return $this;
}
protected function tick()
{
if (!($this->startMode & static::START_MODE_REPLACE))
{
$this->tickStreams();
}
$this->tickProcessStatus();
}
protected function tickStreams()
{
// Pump streams if necessary
if (!isset($this->stdout) || !is_array($this->stdout))
{
$this->stdout = [];
}
while ($line = fgets($this->stream_stdout))
{
// This array is initialized a few lines above and not set anywhere else.
// @phan-suppress-next-line PhanTypeMismatchDimEmpty
$this->stdout[] = trim($line);
foreach ($this->outputCallbacks as $callback)
{
if (($callback[2] & static::STREAM_STDOUT) == 0)
{
continue;
}
if (isset($callback[1]))
{
if (preg_match($callback[1], $line, $matches))
{
call_user_func($callback[0], $this, $line, $matches);
}
}
else
{
call_user_func($callback[0], $this, $line);
}
}
}
if (!isset($this->stderr) || !is_array($this->stderr))
{
$this->stderr = [];
}
while ($line = fgets($this->stream_stderr))
{
// This array is initialized a few lines above and not set anywhere else.
// @phan-suppress-next-line PhanTypeMismatchDimEmpty
$this->stderr[] = trim($line);
foreach ($this->outputCallbacks as $callback)
{
if (($callback[2] & static::STREAM_STDERR) == 0)
{
continue;
}
if (isset($callback[1]))
{
if (preg_match($callback[1], $line, $matches))
{
call_user_func($callback[0], $this, $line, $matches);
}
}
else
{
call_user_func($callback[0], $this, $line);
}
}
}
}
protected function tickProcessStatus()
{
// Check current status
$status = proc_get_status($this->handle);
if (isset($status['exitcode']) && $status['exitcode'] !== -1)
{
$this->exitStatus = $status['exitcode'];
}
if ($status['running'] === false && $this->status == static::STATUS_RUNNING)
{
$this->stopping();
}
elseif ($this->status == static::STATUS_STOPPED)
{
$this->status = static::STATUS_EXITED;
if ($this->startMode & static::START_MODE_ASYNC)
{
unregister_tick_function([&$this, 'tick']);
}
$this->exited();
}
}
protected function stopping()
{
$this->status = static::STATUS_STOPPING;
foreach ($this->exitCallback as $exitCallback)
{
call_user_func($exitCallback, $this);
}
$this->status = static::STATUS_STOPPED;
}
protected function exited()
{
if (!($this->startMode & static::START_MODE_REPLACE))
{
fclose($this->stream_stdin);
fclose($this->stream_stdout);
fclose($this->stream_stderr);
}
proc_close($this->handle);
if ($this->raiseExceptionOnFailure && $this->exitStatus != 0)
{
throw new \Exception('Command execution failed ('.$this->exitStatus.'): '.implode(' ', $this->command).PHP_EOL.
'Stdout:'.PHP_EOL.
$this->getStdoutString().PHP_EOL.
'Stderr:'.PHP_EOL.
$this->getStderrString().PHP_EOL);
}
}
public function write($data)
{
fwrite($this->stream_stdin, $data);
return $this;
}
public function close()
{
fclose($this->stream_stdin);
return $this;
}
protected function getCommand()
{
$processed = [];
$command = $this->command;
$commandName = array_shift($command);
$processed[] = strpos($commandName, ' ') === false ? $commandName : '"'.$commandName.'"';
foreach ($command as $part)
{
if (is_object($part) && $part instanceof CommandArgument)
{
$processed[] = $part->get();
}
else
{
$processed[] = $part;
}
}
return implode(' ', $processed);
}
}

18
app/System/RawCommandArgument.php

@ -1,18 +0,0 @@
<?php
declare(strict_types=1);
namespace App\System;
class RawCommandArgument extends CommandArgument
{
public function get()
{
return $this->value;
}
public function __toString()
{
return $this->value;
}
}

13
app/System/SensitiveCommandArgument.php

@ -1,13 +0,0 @@
<?php
declare(strict_types=1);
namespace App\System;
class SensitiveCommandArgument extends CommandArgument
{
public function __toString()
{
return '<'.escapeshellarg(str_repeat('*', strlen($this->value))).'>';
}
}

8
composer.json

@ -17,6 +17,7 @@
"require": {
"php": "^7.2.0",
"laravel-zero/framework": "^7.0",
"nucleardog/processes": "^0.0.1",
"symfony/yaml": "^5.0"
},
"require-dev": {
@ -44,9 +45,12 @@
"post-create-project-cmd": [
"@php application app:rename"
],
"post-autoload-dump": "cp .githooks/* .git/hooks"
"post-autoload-dump": "if [ -d .githooks ]; then cp .githooks/* .git/hooks; fi"
},
"minimum-stability": "dev",
"prefer-stable": true,
"bin": ["monitor_layout"]
"bin": ["monitor_layout"],
"repositories": [
{ "type": "composer", "url": "https://composer.nucleardog.ca/" }
]
}

45
composer.lock

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "c029b92d27ab874462d9886d60030953",
"content-hash": "9cbc4d954112538271c7a41ad0c67298",
"packages": [
{
"name": "doctrine/inflector",
@ -1021,6 +1021,49 @@
],
"time": "2020-04-20T15:05:43+00:00"
},
{
"name": "nucleardog/processes",
"version": "v0.0.1",
"source": {
"type": "git",
"url": "ssh://git@git.nucleardog.ca/nucleardog/processes",
"reference": "8995dbb5f0de7eab45ca9fa1f7d6beed6622ad1a"
},
"require": {
"php": "^7.2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.16",
"phan/phan": "^2.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Processes\\": "src/"
}
},
"scripts": {
"post-autoload-dump": [
"if [ -d .githooks ]; then cp .githooks/* .git/hooks/; fi"
]
},
"license": [
"proprietary"
],
"authors": [
{
"name": "Adam Pippin",
"email": "hello@adampippin.ca"
}
],
"description": "Library for invoking external commands and doing useful things with them",
"homepage": "https://adampippin.ca/",
"support": {
"issues": "https://git.nucleardog.ca/nucleardog/processes/issues",
"source": "https://git.nucleardog.ca/nucleardog/processes"
},
"time": "2020-05-01T23:23:11+00:00"
},
{
"name": "nunomaduro/collision",
"version": "v4.2.0",

Loading…
Cancel
Save