dockza.app 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HelpOverlay = void 0;
7
+ const neo_blessed_1 = __importDefault(require("neo-blessed"));
8
+ const _theme_1 = require("../theme");
9
+ class HelpOverlay {
10
+ screen;
11
+ box;
12
+ visible = false;
13
+ hideHandlers = [];
14
+ handleClose = () => {
15
+ if (!this.visible)
16
+ return;
17
+ this.hide();
18
+ };
19
+ constructor(screen) {
20
+ this.screen = screen;
21
+ this.box = neo_blessed_1.default.box({
22
+ parent: screen,
23
+ top: 'center',
24
+ left: 'center',
25
+ width: '70%',
26
+ height: '85%',
27
+ tags: true,
28
+ scrollable: true,
29
+ alwaysScroll: true,
30
+ mouse: true,
31
+ keys: true,
32
+ vi: true,
33
+ border: { type: 'line' },
34
+ style: {
35
+ bg: _theme_1.C.bgSoft,
36
+ border: { fg: _theme_1.C.accent },
37
+ },
38
+ hidden: true,
39
+ });
40
+ screen.append(this.box);
41
+ }
42
+ on(event, handler) {
43
+ if (event === 'hide')
44
+ this.hideHandlers.push(handler);
45
+ }
46
+ show() {
47
+ this.visible = true;
48
+ this.box.setContent(this.buildContent());
49
+ this.box.show();
50
+ this.box.setFront();
51
+ this.box.focus();
52
+ this.screen.key(['escape'], this.handleClose);
53
+ this.screen.render();
54
+ }
55
+ hide() {
56
+ this.visible = false;
57
+ this.box.hide();
58
+ this.screen.removeKey('escape', this.handleClose);
59
+ this.hideHandlers.forEach((h) => h());
60
+ this.screen.render();
61
+ }
62
+ isVisible() {
63
+ return this.visible;
64
+ }
65
+ buildContent() {
66
+ const KW = 18;
67
+ const row = (key, desc) => ` ${_theme_1.t.aqua(key.padEnd(KW))}${_theme_1.t.fg(desc)}`;
68
+ const section = (name) => `\n ${_theme_1.t.accent(name)}\n`;
69
+ return [
70
+ ` {bold}${_theme_1.t.accent('dockza — key bindings')}{/bold}`,
71
+ section('Navigation'),
72
+ row('1 … 5', 'Jump to Stacks / Containers / Images / Volumes / Networks'),
73
+ row('Tab / Shift+Tab', 'Cycle views'),
74
+ row('h', 'Toggle this help'),
75
+ row('q / Ctrl+C', 'Quit'),
76
+ section('Stacks view'),
77
+ row('↑ ↓ / j k', 'Navigate the tree'),
78
+ row('→', 'Expand the selected stack'),
79
+ row('←', 'Collapse the selected stack (or jump to its header)'),
80
+ row('Enter', 'On a stack: toggle expansion. On a service: open detail.'),
81
+ row('l', 'Open log viewer for the selected service'),
82
+ row('x', "Open an external terminal exec'd into the service"),
83
+ row('s / r / k', 'Stop / restart / kill the running service (confirm dialog)'),
84
+ row('S', 'Start a stopped service'),
85
+ row('d', 'Remove a stopped service (confirm dialog)'),
86
+ row('/', 'Filter stacks and services (Esc cancels)'),
87
+ section('Containers view'),
88
+ row('↑ ↓ / j k', 'Navigate list'),
89
+ row('Enter', 'Open detail panel'),
90
+ row('l', 'Open log viewer'),
91
+ row('x', 'Shell into container'),
92
+ row('s / r / k', 'Stop / restart / kill running container'),
93
+ row('S', 'Start stopped container'),
94
+ row('d', 'Remove stopped container'),
95
+ section('Detail panel'),
96
+ row('e', 'Toggle environment variables'),
97
+ row('l', 'Open log viewer'),
98
+ row('s / r / k', 'Stop / restart / kill running container (confirm)'),
99
+ row('S', 'Start stopped container'),
100
+ row('d', 'Remove stopped container (confirm)'),
101
+ row('x', 'Shell into running container'),
102
+ row('↑ ↓', 'Scroll detail content'),
103
+ row('Esc', 'Close detail panel'),
104
+ section('Log viewer'),
105
+ row('f', 'Toggle follow mode'),
106
+ row('g', 'Scroll to top'),
107
+ row('G', 'Scroll to bottom (re-enables follow)'),
108
+ row('↑ / k', 'Scroll up (disables follow)'),
109
+ row('Esc', 'Close log viewer'),
110
+ section('Help overlay'),
111
+ row('h', 'Toggle this help (open or close)'),
112
+ row('Esc', 'Close this help'),
113
+ section('Images, Volumes & Networks'),
114
+ row('↑ ↓ / j k', 'Navigate list'),
115
+ row('d', 'Delete unused item'),
116
+ section('Confirm dialog'),
117
+ row('y / Y', 'Confirm action'),
118
+ row('n / N / Esc', 'Cancel'),
119
+ ].join('\n');
120
+ }
121
+ }
122
+ exports.HelpOverlay = HelpOverlay;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ImagesTab = void 0;
4
+ const images_1 = require("../../docker/images");
5
+ const _theme_1 = require("../../theme");
6
+ const format_1 = require("../../utils/format");
7
+ const resource_list_tab_1 = require("../resource-list-tab");
8
+ class ImagesTab extends resource_list_tab_1.ResourceListTab {
9
+ constructor(screen, dims) {
10
+ super(screen, dims, {
11
+ list: images_1.listImages,
12
+ remove: (img) => (0, images_1.removeImage)(img.id),
13
+ getKey: (img) => img.id,
14
+ emptyMessage: 'No images',
15
+ confirmTitle: 'Remove image?',
16
+ confirmLabel: (img) => `${img.repository}:${img.tag}`,
17
+ guards: [(img) => (img.inUse ? 'Image in use — cannot delete' : null)],
18
+ columns: [
19
+ { header: 'REPOSITORY', weight: 0.25, render: (img, w) => (0, format_1.truncate)(img.repository, w - 1) },
20
+ { header: 'TAG', weight: 0.12, render: (img, w) => _theme_1.t.cyan((0, format_1.truncate)(img.tag, w - 1)) },
21
+ {
22
+ header: 'ID',
23
+ weight: 0.1,
24
+ render: (img) => _theme_1.t.comment(img.id.replace('sha256:', '').slice(0, 12)),
25
+ },
26
+ { header: 'CREATED', weight: 0.15, render: (img) => _theme_1.t.comment((0, format_1.relativeTime)(img.created)) },
27
+ { header: 'SIZE', weight: 0.1, render: (img) => (0, format_1.humanSizeMB)(img.sizeMB) },
28
+ {
29
+ header: 'STATUS',
30
+ weight: 0,
31
+ render: (img) => (img.inUse ? _theme_1.t.green('● in use') : _theme_1.t.red('○ unused')),
32
+ },
33
+ ],
34
+ });
35
+ }
36
+ }
37
+ exports.ImagesTab = ImagesTab;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NetworksTab = void 0;
4
+ const networks_1 = require("../../docker/networks");
5
+ const _theme_1 = require("../../theme");
6
+ const format_1 = require("../../utils/format");
7
+ const resource_list_tab_1 = require("../resource-list-tab");
8
+ class NetworksTab extends resource_list_tab_1.ResourceListTab {
9
+ constructor(screen, dims) {
10
+ super(screen, dims, {
11
+ list: networks_1.listNetworks,
12
+ remove: (net) => (0, networks_1.removeNetwork)(net.id),
13
+ getKey: (net) => net.id,
14
+ emptyMessage: 'No networks',
15
+ confirmTitle: 'Remove network?',
16
+ confirmLabel: (net) => net.name,
17
+ guards: [
18
+ (net) => (net.builtin ? 'Built-in network — cannot delete' : null),
19
+ (net) => (net.inUse ? 'Network in use — cannot delete' : null),
20
+ ],
21
+ columns: [
22
+ { header: 'NAME', weight: 0.22, render: (net, w) => (0, format_1.truncate)(net.name, w - 1) },
23
+ { header: 'DRIVER', weight: 0.12, render: (net, w) => (0, format_1.truncate)(net.driver, w - 1) },
24
+ { header: 'SCOPE', weight: 0.1, render: (net, w) => _theme_1.t.comment((0, format_1.truncate)(net.scope, w - 1)) },
25
+ { header: 'ID', weight: 0.14, render: (net) => _theme_1.t.comment(net.id.slice(0, 12)) },
26
+ {
27
+ header: 'CTNRS',
28
+ weight: 0.09,
29
+ render: (net) => (net.containerCount > 0 ? String(net.containerCount) : _theme_1.t.faint('0')),
30
+ },
31
+ {
32
+ header: 'CREATED',
33
+ weight: 0.15,
34
+ render: (net) => net.created.getTime() > 0 ? _theme_1.t.comment((0, format_1.relativeTime)(net.created)) : _theme_1.t.comment('—'),
35
+ },
36
+ {
37
+ header: 'STATUS',
38
+ weight: 0,
39
+ render: (net) => (net.inUse ? _theme_1.t.green('● in use') : _theme_1.t.red('○ unused')),
40
+ },
41
+ ],
42
+ });
43
+ }
44
+ }
45
+ exports.NetworksTab = NetworksTab;
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ResourceListTab = void 0;
7
+ const neo_blessed_1 = __importDefault(require("neo-blessed"));
8
+ const confirm_dialog_1 = require("./containers/confirm-dialog");
9
+ const _theme_1 = require("../theme");
10
+ const format_1 = require("../utils/format");
11
+ const widgets_1 = require("./widgets");
12
+ /**
13
+ * Shared machinery for the flat "resource list" tabs (Images / Volumes / Networks):
14
+ * a header bar over a bordered list, `d`-to-delete with a confirm dialog and guards,
15
+ * selection preservation across polls, and navigate/error events. Concrete tabs supply
16
+ * only their columns, guards, confirm strings, and list/remove functions via config.
17
+ */
18
+ class ResourceListTab {
19
+ config;
20
+ screen;
21
+ wrapper;
22
+ headerBox;
23
+ list;
24
+ messageBox;
25
+ confirmDialog;
26
+ items = [];
27
+ active = false;
28
+ updating = false;
29
+ errorHandlers = [];
30
+ navigateHandlers = [];
31
+ handleD = () => {
32
+ if (!this.active || this.confirmDialog.isVisible())
33
+ return;
34
+ const item = this.items[(0, widgets_1.listSelected)(this.list)];
35
+ if (!item)
36
+ return;
37
+ for (const guard of this.config.guards) {
38
+ const msg = guard(item);
39
+ if (msg) {
40
+ this.emitError(msg);
41
+ return;
42
+ }
43
+ }
44
+ this.confirmDialog.show({
45
+ title: this.config.confirmTitle,
46
+ message: `${this.config.confirmLabel(item)} will be permanently deleted.`,
47
+ danger: true,
48
+ onConfirm: () => {
49
+ void this.config
50
+ .remove(item)
51
+ .then(() => this.refresh())
52
+ .catch((err) => this.emitError(err))
53
+ .finally(() => {
54
+ this.list.focus();
55
+ this.screen.render();
56
+ });
57
+ },
58
+ onCancel: () => {
59
+ this.list.focus();
60
+ this.screen.render();
61
+ },
62
+ });
63
+ };
64
+ constructor(screen, dims, config) {
65
+ this.config = config;
66
+ this.screen = screen;
67
+ this.wrapper = neo_blessed_1.default.box({
68
+ parent: screen,
69
+ top: dims.top,
70
+ left: dims.left,
71
+ width: dims.width,
72
+ height: dims.height,
73
+ hidden: true,
74
+ });
75
+ this.headerBox = (0, widgets_1.createHeaderBar)(this.wrapper);
76
+ this.list = (0, widgets_1.createListWidget)(this.wrapper);
77
+ this.messageBox = (0, widgets_1.createCenteredMessage)(this.wrapper);
78
+ this.list.on('select item', (_item, index) => {
79
+ if (!this.updating) {
80
+ this.navigateHandlers.forEach((h) => h(this.items[index] ?? null));
81
+ }
82
+ });
83
+ this.confirmDialog = new confirm_dialog_1.ConfirmDialog(screen);
84
+ }
85
+ on(event, handler) {
86
+ if (event === 'error')
87
+ this.errorHandlers.push(handler);
88
+ if (event === 'navigate')
89
+ this.navigateHandlers.push(handler);
90
+ }
91
+ isConfirmOpen() {
92
+ return this.confirmDialog.isVisible();
93
+ }
94
+ showLoading() {
95
+ this.messageBox.setContent(_theme_1.t.comment(' Loading...'));
96
+ this.messageBox.show();
97
+ }
98
+ show() {
99
+ this.active = true;
100
+ this.wrapper.show();
101
+ this.list.focus();
102
+ this.screen.key(['d'], this.handleD);
103
+ this.emitNavigate();
104
+ }
105
+ hide() {
106
+ this.active = false;
107
+ this.wrapper.hide();
108
+ if (this.confirmDialog.isVisible())
109
+ this.confirmDialog.hide();
110
+ this.screen.removeKey('d', this.handleD);
111
+ }
112
+ setData(items) {
113
+ this.updating = true;
114
+ const prevIdx = (0, widgets_1.listSelected)(this.list);
115
+ const prev = this.items[prevIdx];
116
+ const prevKey = prev ? this.config.getKey(prev) : undefined;
117
+ this.items = items;
118
+ this.messageBox.hide();
119
+ const innerWidth = Math.max(10, this.list.width - 2);
120
+ const widths = this.calcWidths(innerWidth);
121
+ this.renderHeader(widths);
122
+ const rows = items.map((item) => this.buildRow(item, widths));
123
+ this.list.setItems(rows);
124
+ const foundIdx = prevKey !== undefined ? items.findIndex((it) => this.config.getKey(it) === prevKey) : -1;
125
+ const targetIdx = Math.min(foundIdx >= 0 ? foundIdx : prevIdx, Math.max(0, items.length - 1));
126
+ this.list.select(targetIdx);
127
+ this.updating = false;
128
+ if (items.length === 0) {
129
+ this.messageBox.setContent(_theme_1.t.comment(` ${this.config.emptyMessage}`));
130
+ this.messageBox.show();
131
+ }
132
+ this.emitNavigate();
133
+ }
134
+ async refresh() {
135
+ const items = await this.config.list();
136
+ this.setData(items);
137
+ }
138
+ /** Re-render the current dataset (useful after terminal resize). */
139
+ redraw() {
140
+ this.setData(this.items);
141
+ }
142
+ emitNavigate() {
143
+ this.navigateHandlers.forEach((h) => h(this.items[(0, widgets_1.listSelected)(this.list)] ?? null));
144
+ }
145
+ emitError(err) {
146
+ const msg = err instanceof Error ? err.message : String(err);
147
+ this.errorHandlers.forEach((h) => h(msg));
148
+ }
149
+ calcWidths(inner) {
150
+ const cols = this.config.columns;
151
+ const widths = [];
152
+ let used = 0;
153
+ for (let i = 0; i < cols.length; i++) {
154
+ if (i === cols.length - 1) {
155
+ widths.push(Math.max(1, inner - used));
156
+ }
157
+ else {
158
+ const w = Math.floor(inner * cols[i].weight);
159
+ widths.push(w);
160
+ used += w;
161
+ }
162
+ }
163
+ return widths;
164
+ }
165
+ renderHeader(widths) {
166
+ const cols = this.config.columns;
167
+ const parts = cols.map((col, i) => {
168
+ const w = widths[i];
169
+ // First column carries a leading space and pads to w+1 (the list body indents
170
+ // its rows by one), matching the data offset below.
171
+ if (i === 0)
172
+ return (0, format_1.padEnd)(_theme_1.t.comment(` ${(0, format_1.truncate)(col.header, w - 1)}`), w + 1);
173
+ if (i === cols.length - 1)
174
+ return _theme_1.t.comment((0, format_1.truncate)(col.header, w));
175
+ return (0, format_1.padEnd)(_theme_1.t.comment((0, format_1.truncate)(col.header, w)), w);
176
+ });
177
+ this.headerBox.setContent(parts.join(''));
178
+ }
179
+ buildRow(item, widths) {
180
+ return this.config.columns.map((col, i) => (0, format_1.padEnd)(col.render(item, widths[i]), widths[i])).join('');
181
+ }
182
+ }
183
+ exports.ResourceListTab = ResourceListTab;
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SideRail = exports.RAIL_WIDTH = void 0;
7
+ const neo_blessed_1 = __importDefault(require("neo-blessed"));
8
+ const _theme_1 = require("../theme");
9
+ const format_1 = require("../utils/format");
10
+ const VIEWS = [
11
+ { id: 'stacks', label: 'Stacks', icon: '◰', key: '1' },
12
+ { id: 'containers', label: 'Containers', icon: '◧', key: '2' },
13
+ { id: 'images', label: 'Images', icon: '◩', key: '3' },
14
+ { id: 'volumes', label: 'Volumes', icon: '◪', key: '4' },
15
+ { id: 'networks', label: 'Networks', icon: '◫', key: '5' },
16
+ ];
17
+ exports.RAIL_WIDTH = 28;
18
+ class SideRail {
19
+ box;
20
+ activeView = 'stacks';
21
+ counts = { stacks: 0, containers: 0, images: 0, volumes: 0, networks: 0 };
22
+ stacks = [];
23
+ clickable = [];
24
+ viewChangeHandlers = [];
25
+ stackJumpHandlers = [];
26
+ constructor(screen) {
27
+ this.box = neo_blessed_1.default.box({
28
+ parent: screen,
29
+ top: 1,
30
+ left: 0,
31
+ width: exports.RAIL_WIDTH,
32
+ height: '100%-2',
33
+ tags: true,
34
+ mouse: true,
35
+ style: { bg: _theme_1.C.bg, fg: _theme_1.C.fg },
36
+ });
37
+ this.box.on('click', (data) => {
38
+ const top = Number(this.box.atop ?? 1);
39
+ const relRow = data.y - top;
40
+ const hit = this.clickable.find((c) => c.row === relRow);
41
+ if (!hit)
42
+ return;
43
+ if (hit.kind === 'view')
44
+ this.viewChangeHandlers.forEach((h) => h(hit.payload));
45
+ else if (hit.kind === 'stack')
46
+ this.stackJumpHandlers.forEach((h) => h(hit.payload));
47
+ });
48
+ }
49
+ on(event, handler) {
50
+ if (event === 'view-change')
51
+ this.viewChangeHandlers.push(handler);
52
+ else if (event === 'stack-jump')
53
+ this.stackJumpHandlers.push(handler);
54
+ }
55
+ setActiveView(id) {
56
+ this.activeView = id;
57
+ this.render();
58
+ }
59
+ setCounts(counts) {
60
+ this.counts = counts;
61
+ this.render();
62
+ }
63
+ setStacks(stacks) {
64
+ this.stacks = stacks;
65
+ this.render();
66
+ }
67
+ getViews() {
68
+ return VIEWS;
69
+ }
70
+ render() {
71
+ const lines = [];
72
+ this.clickable = [];
73
+ lines.push(_theme_1.t.dim(' VIEW'));
74
+ for (const v of VIEWS) {
75
+ const idx = lines.length;
76
+ lines.push(this.renderViewRow(v));
77
+ this.clickable.push({ row: idx, kind: 'view', payload: v.id });
78
+ }
79
+ lines.push('');
80
+ lines.push(_theme_1.t.dim(' STACKS'));
81
+ const stackLines = this.renderStacks();
82
+ for (const sl of stackLines) {
83
+ const idx = lines.length;
84
+ lines.push(sl.text);
85
+ if (sl.stackId)
86
+ this.clickable.push({ row: idx, kind: 'stack', payload: sl.stackId });
87
+ }
88
+ this.box.setContent(lines.join('\n'));
89
+ }
90
+ renderViewRow(v) {
91
+ const count = String(this.counts[v.id]);
92
+ const isActive = this.activeView === v.id;
93
+ const labelWidth = exports.RAIL_WIDTH - 7; // 2 indent + icon + space + count budget(3) + trailing
94
+ const labelRaw = (0, format_1.truncate)(v.label, labelWidth);
95
+ if (isActive) {
96
+ const edge = `{${_theme_1.C.accent}-fg}▌{/}`;
97
+ const inner = ` ${_theme_1.t.accent(v.icon)} ${_theme_1.t.fg(labelRaw)}`;
98
+ const right = _theme_1.t.dim(count);
99
+ const padded = (0, format_1.padEnd)(`${edge}${inner}`, exports.RAIL_WIDTH - (0, format_1.visualLength)(right) - 1);
100
+ return `${padded}${right} `;
101
+ }
102
+ const inner = ` ${_theme_1.t.dim(v.icon)} ${_theme_1.t.dim(labelRaw)}`;
103
+ const right = _theme_1.t.faint(count);
104
+ const padded = (0, format_1.padEnd)(inner, exports.RAIL_WIDTH - (0, format_1.visualLength)(right) - 1);
105
+ return `${padded}${right} `;
106
+ }
107
+ renderStacks() {
108
+ if (this.stacks.length === 0) {
109
+ return [{ text: ` ${_theme_1.t.faint('(none)')}` }];
110
+ }
111
+ // Cap the visible stacks list; if there's overflow, show a "+N more" tail.
112
+ const MAX = 10;
113
+ const lines = [];
114
+ const shown = this.stacks.slice(0, MAX);
115
+ for (const s of shown) {
116
+ const dot = s.isLive ? _theme_1.t.green('●') : _theme_1.t.faint('○');
117
+ const labelMax = exports.RAIL_WIDTH - 8;
118
+ const name = (0, format_1.truncate)(s.id, labelMax);
119
+ const nameStyled = s.isLive ? _theme_1.t.fg(name) : _theme_1.t.dim(name);
120
+ const count = _theme_1.t.faint(String(s.services.length));
121
+ const left = ` ${dot} ${nameStyled}`;
122
+ const padded = (0, format_1.padEnd)(left, exports.RAIL_WIDTH - (0, format_1.visualLength)(count) - 1);
123
+ lines.push({ text: `${padded}${count} `, stackId: s.id });
124
+ }
125
+ const overflow = this.stacks.length - shown.length;
126
+ if (overflow > 0) {
127
+ lines.push({ text: _theme_1.t.faint(` +${overflow} more…`) });
128
+ }
129
+ return lines;
130
+ }
131
+ }
132
+ exports.SideRail = SideRail;