TmuxLayoutBuilder::createOptionalWindows()   F
last analyzed

Complexity

Conditions 20
Paths 640

Size

Total Lines 86
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 50
c 1
b 0
f 0
dl 0
loc 86
rs 0.5
cc 20
nc 640
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace App\Services\Tmux;
4
5
use App\Models\Settings;
6
7
/**
8
 * Service for building tmux window layouts based on sequential mode
9
 */
10
class TmuxLayoutBuilder
11
{
12
    protected TmuxSessionManager $sessionManager;
13
14
    protected TmuxPaneManager $paneManager;
15
16
    protected string $sessionName;
17
18
    public function __construct(TmuxSessionManager $sessionManager)
19
    {
20
        $this->sessionManager = $sessionManager;
21
        $this->sessionName = $sessionManager->getSessionName();
22
        $this->paneManager = new TmuxPaneManager($this->sessionName);
23
    }
24
25
    /**
26
     * Build the appropriate layout based on sequential mode
27
     */
28
    public function buildLayout(int $sequentialMode): bool
29
    {
30
        return match ($sequentialMode) {
31
            1 => $this->buildBasicLayout(),
32
            2 => $this->buildStrippedLayout(),
33
            default => $this->buildFullLayout(),
34
        };
35
    }
36
37
    /**
38
     * Build full non-sequential layout (mode 0)
39
     */
40
    protected function buildFullLayout(): bool
41
    {
42
        // Window 0: Monitor + Binaries + Backfill + Releases
43
        if (! $this->sessionManager->createSession('Monitor')) {
44
            return false;
45
        }
46
47
        // Select pane 0.0, then split for binaries (right side, 67%)
48
        $this->paneManager->selectPane('0.0');
49
        $this->paneManager->splitHorizontal('0', '67%', 'update_binaries');
50
51
        // Select pane 0.1 (binaries), then split for backfill (bottom, 67%)
52
        $this->paneManager->selectPane('0.1');
53
        $this->paneManager->splitVertical('0', '67%', 'backfill');
54
55
        // Split again for releases (bottom, 50%)
56
        $this->paneManager->splitVertical('0', '50%', 'update_releases');
57
58
        // Window 1: Fix names + Remove crap
59
        $this->paneManager->createWindow(1, 'utils');
60
        $this->paneManager->setPaneTitle('1.0', 'fixReleaseNames');
61
        $this->paneManager->selectPane('1.0');
62
        $this->paneManager->splitHorizontal('1', '50%', 'removeCrapReleases');
63
64
        // Window 2: Postprocessing (Left: Additional + TV + Amazon, Right: Movies + XXX)
65
        $this->paneManager->createWindow(2, 'post');
66
        $this->paneManager->setPaneTitle('2.0', 'postprocessing_additional');
67
68
        // Split horizontally to create left and right halves (don't set title yet)
69
        $this->paneManager->splitHorizontal('2', '50%', '');
70
71
        // Left side (2.0): split vertically for TV and Amazon
72
        $this->paneManager->selectPane('2.0');
73
        $this->paneManager->splitVertical('2', '67%', 'postprocessing_tv');
74
        $this->paneManager->splitVertical('2', '50%', 'postprocessing_amazon');
75
76
        // Right side: After left splits, right side is now 2.3
77
        $this->paneManager->selectPane('2.3');
78
        $this->paneManager->setPaneTitle('2.3', 'postprocessing_movies');
79
        $this->paneManager->splitVertical('2', '50%', 'postprocessing_xxx');
80
81
        // Window 3: IRC Scraper
82
        $this->createIRCScraperWindow();
83
84
        // Additional windows (optional monitoring tools)
85
        $this->createOptionalWindows();
86
87
        return true;
88
    }
89
90
    /**
91
     * Build basic sequential layout (mode 1)
92
     */
93
    protected function buildBasicLayout(): bool
94
    {
95
        // Window 0: Monitor + Releases
96
        if (! $this->sessionManager->createSession('Monitor')) {
97
            return false;
98
        }
99
100
        // Select pane 0.0, then split for releases
101
        $this->paneManager->selectPane('0.0');
102
        $this->paneManager->splitHorizontal('0', '67%', 'update_releases');
103
104
        // Window 1: Utils (Fix names + Remove crap)
105
        $this->paneManager->createWindow(1, 'utils');
106
        $this->paneManager->setPaneTitle('1.0', 'fixReleaseNames');
107
        $this->paneManager->selectPane('1.0');
108
        $this->paneManager->splitHorizontal('1', '50%', 'removeCrapReleases');
109
110
        // Window 2: Postprocessing (Left: Additional + TV + Amazon, Right: Movies + XXX)
111
        $this->paneManager->createWindow(2, 'post');
112
        $this->paneManager->setPaneTitle('2.0', 'postprocessing_additional');
113
114
        // Split horizontally to create left and right halves (don't set title yet)
115
        $this->paneManager->splitHorizontal('2', '50%', '');
116
117
        // Left side (2.0): split vertically for TV and Amazon
118
        $this->paneManager->selectPane('2.0');
119
        $this->paneManager->splitVertical('2', '67%', 'postprocessing_tv');
120
        $this->paneManager->splitVertical('2', '50%', 'postprocessing_amazon');
121
122
        // Right side: After left splits, right side is now 2.3
123
        $this->paneManager->selectPane('2.3');
124
        $this->paneManager->setPaneTitle('2.3', 'postprocessing_movies');
125
        $this->paneManager->splitVertical('2', '50%', 'postprocessing_xxx');
126
127
        // Window 3: IRC Scraper
128
        $this->createIRCScraperWindow();
129
130
        $this->createOptionalWindows();
131
132
        return true;
133
    }
134
135
    /**
136
     * Build stripped sequential layout (mode 2)
137
     */
138
    protected function buildStrippedLayout(): bool
139
    {
140
        // Window 0: Monitor + Sequential
141
        if (! $this->sessionManager->createSession('Monitor')) {
142
            return false;
143
        }
144
145
        // Select pane 0.0, then split for sequential
146
        $this->paneManager->selectPane('0.0');
147
        $this->paneManager->splitHorizontal('0', '67%', 'sequential');
148
149
        // Window 1: Amazon postprocessing
150
        $this->paneManager->createWindow(1, 'utils');
151
        $this->paneManager->setPaneTitle('1.0', 'fixReleaseNames');
152
        $this->paneManager->selectPane('1.0');
153
        $this->paneManager->splitHorizontal('1', '50%', 'postprocessing_amazon');
154
155
        // Window 2: IRC Scraper
156
        $this->createIRCScraperWindow();
157
158
        $this->createOptionalWindows();
159
160
        return true;
161
    }
162
163
    /**
164
     * Create IRC scraper window
165
     */
166
    protected function createIRCScraperWindow(): bool
167
    {
168
        $this->paneManager->createWindow(3, 'IRCScraper');
169
        $this->paneManager->setPaneTitle('3.0', 'scrapeIRC');
170
171
        return true;
172
    }
173
174
    /**
175
     * Create optional monitoring windows based on settings
176
     *
177
     * Creates separate tmux windows for enabled monitoring tools.
178
     * Each tool gets its own dedicated window starting from index 4.
179
     *
180
     * Window layout:
181
     * - Window 0: Monitor + Processing panes (binaries/backfill/releases)
182
     * - Window 1: Utilities (fix names, remove crap)
183
     * - Window 2: Postprocessing (additional, tv/anime, movies, amazon)
184
     * - Window 3: IRC Scraper
185
     * - Window 4+: Monitoring tools (htop, nmon, vnstat, tcptrack, bwm-ng, mytop, redis, console)
186
     */
187
    protected function createOptionalWindows(): void
188
    {
189
        $windowIndex = 4;
190
191
        // htop
192
        if ((int) Settings::settingValue('htop') === 1 && $this->commandExists('htop')) {
193
            $this->paneManager->createWindow($windowIndex, 'htop');
194
            $this->paneManager->respawnPane("{$windowIndex}.0", 'htop');
195
            $windowIndex++;
196
        }
197
198
        // nmon
199
        if ((int) Settings::settingValue('nmon') === 1 && $this->commandExists('nmon')) {
200
            $this->paneManager->createWindow($windowIndex, 'nmon');
201
            $this->paneManager->respawnPane("{$windowIndex}.0", 'nmon -t');
202
            $windowIndex++;
203
        }
204
205
        // vnstat
206
        if ((int) Settings::settingValue('vnstat') === 1 && $this->commandExists('vnstat')) {
207
            $vnstatArgs = Settings::settingValue('vnstat_args') ?? '';
208
            $this->paneManager->createWindow($windowIndex, 'vnstat');
209
            $this->paneManager->respawnPane("{$windowIndex}.0", "watch -n10 'vnstat {$vnstatArgs}'");
210
            $windowIndex++;
211
        }
212
213
        // tcptrack
214
        if ((int) Settings::settingValue('tcptrack') === 1 && $this->commandExists('tcptrack')) {
215
            $tcptrackArgs = Settings::settingValue('tcptrack_args') ?? '';
216
            $this->paneManager->createWindow($windowIndex, 'tcptrack');
217
            $this->paneManager->respawnPane("{$windowIndex}.0", "tcptrack {$tcptrackArgs}");
218
            $windowIndex++;
219
        }
220
221
        // bwm-ng
222
        if ((int) Settings::settingValue('bwmng') === 1 && $this->commandExists('bwm-ng')) {
223
            $this->paneManager->createWindow($windowIndex, 'bwm-ng');
224
            $this->paneManager->respawnPane("{$windowIndex}.0", 'bwm-ng');
225
            $windowIndex++;
226
        }
227
228
        // mytop
229
        if ((int) Settings::settingValue('mytop') === 1 && $this->commandExists('mytop')) {
230
            $this->paneManager->createWindow($windowIndex, 'mytop');
231
            $this->paneManager->respawnPane("{$windowIndex}.0", 'mytop -u');
232
            $windowIndex++;
233
        }
234
235
        // redis monitoring
236
        if ((int) Settings::settingValue('redis') === 1) {
237
            $redisArgs = Settings::settingValue('redis_args') ?? '';
238
            $refreshInterval = 30;
239
240
            $this->paneManager->createWindow($windowIndex, 'redis');
241
242
            // Check if custom args provided for simple redis-cli output
243
            if (! empty($redisArgs) && $redisArgs !== 'NULL' && $this->commandExists('redis-cli')) {
244
                $redisHost = config('database.redis.default.host', '127.0.0.1');
245
                $redisPort = config('database.redis.default.port', 6379);
246
                $this->paneManager->respawnPane("{$windowIndex}.0", "watch -n{$refreshInterval} -c 'redis-cli -h {$redisHost} -p {$redisPort} {$redisArgs}'");
247
            } else {
248
                // Use PHP-based Redis monitor service
249
                $redisHost = config('database.redis.default.host', '127.0.0.1');
250
                $redisPort = config('database.redis.default.port', 6379);
251
                $artisan = base_path('artisan');
252
253
                // Determine how to connect to Redis
254
                $connectionInfo = $this->resolveRedisConnection($redisHost, (int) $redisPort);
255
256
                if ($connectionInfo['use_sail']) {
257
                    // Use sail to run inside Docker container
258
                    $sail = base_path('sail');
259
                    $this->paneManager->respawnPane("{$windowIndex}.0", "{$sail} artisan redis:monitor --refresh={$refreshInterval}");
260
                } else {
261
                    // Run directly, potentially with host override
262
                    $envPrefix = $connectionInfo['override_host'] ? "REDIS_HOST={$connectionInfo['host']} " : '';
263
                    $this->paneManager->respawnPane("{$windowIndex}.0", "{$envPrefix}php {$artisan} redis:monitor --refresh={$refreshInterval}");
264
                }
265
            }
266
            $windowIndex++;
267
        }
268
269
        // bash console
270
        if ((int) Settings::settingValue('console') === 1) {
271
            $this->paneManager->createWindow($windowIndex, 'bash');
272
            $this->paneManager->respawnPane("{$windowIndex}.0", 'bash -i');
273
        }
274
    }
275
276
    /**
277
     * Check if a command exists
278
     */
279
    protected function commandExists(string $command): bool
280
    {
281
        $result = \Illuminate\Support\Facades\Process::timeout(5)
282
            ->run("which {$command} 2>/dev/null");
283
284
        return $result->successful() && str_contains($result->output(), $command);
285
    }
286
287
    /**
288
     * Resolve how to connect to Redis from the host
289
     *
290
     * Returns an array with:
291
     * - 'use_sail' => bool - whether to use sail to run inside Docker
292
     * - 'override_host' => bool - whether to override REDIS_HOST env var
293
     * - 'host' => string - the host to use (only relevant if override_host is true)
294
     */
295
    protected function resolveRedisConnection(string $host, int $port): array
296
    {
297
        // If host is already an IP or localhost, use it directly
298
        if (filter_var($host, FILTER_VALIDATE_IP) || $host === 'localhost') {
299
            return ['use_sail' => false, 'override_host' => false, 'host' => $host];
300
        }
301
302
        // Try to resolve the hostname
303
        $resolved = gethostbyname($host);
304
        if ($resolved !== $host) {
305
            // Hostname resolves - use it directly
306
            return ['use_sail' => false, 'override_host' => false, 'host' => $host];
307
        }
308
309
        // Hostname doesn't resolve (Docker internal hostname)
310
        // Check if Redis is accessible on 127.0.0.1 (Docker port forwarding)
311
        $socket = @fsockopen('127.0.0.1', $port, $errno, $errstr, 1);
312
        if ($socket !== false) {
313
            fclose($socket);
314
315
            // Redis accessible on localhost - override host to 127.0.0.1
316
            return ['use_sail' => false, 'override_host' => true, 'host' => '127.0.0.1'];
317
        }
318
319
        // Redis not accessible on localhost - need to use sail
320
        return ['use_sail' => true, 'override_host' => false, 'host' => $host];
321
    }
322
}
323