sidekick-docker 0.1.3 → 0.1.5

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.
@@ -39518,22 +39518,32 @@ var require_DockerClient = __commonJS({
39518
39518
  }
39519
39519
  async listContainers(all = true) {
39520
39520
  const containers = await this.docker.listContainers({ all });
39521
- return containers.map((c) => ({
39522
- id: c.Id,
39523
- name: (c.Names[0] || "").replace(/^\//, ""),
39524
- image: c.Image,
39525
- state: c.State,
39526
- status: c.Status,
39527
- ports: (c.Ports || []).map((p) => ({
39528
- hostIp: p.IP || "0.0.0.0",
39529
- hostPort: p.PublicPort || 0,
39530
- containerPort: p.PrivatePort,
39531
- protocol: p.Type || "tcp"
39532
- })),
39533
- created: new Date(c.Created * 1e3),
39534
- composeProject: c.Labels?.["com.docker.compose.project"],
39535
- composeService: c.Labels?.["com.docker.compose.service"]
39536
- }));
39521
+ return containers.map((c) => {
39522
+ let healthStatus;
39523
+ if (/\(healthy\)/.test(c.Status))
39524
+ healthStatus = "healthy";
39525
+ else if (/\(unhealthy\)/.test(c.Status))
39526
+ healthStatus = "unhealthy";
39527
+ else if (/\(health: starting\)/.test(c.Status))
39528
+ healthStatus = "starting";
39529
+ return {
39530
+ id: c.Id,
39531
+ name: (c.Names[0] || "").replace(/^\//, ""),
39532
+ image: c.Image,
39533
+ state: c.State,
39534
+ status: c.Status,
39535
+ ports: (c.Ports || []).map((p) => ({
39536
+ hostIp: p.IP || "0.0.0.0",
39537
+ hostPort: p.PublicPort || 0,
39538
+ containerPort: p.PrivatePort,
39539
+ protocol: p.Type || "tcp"
39540
+ })),
39541
+ created: new Date(c.Created * 1e3),
39542
+ composeProject: c.Labels?.["com.docker.compose.project"],
39543
+ composeService: c.Labels?.["com.docker.compose.service"],
39544
+ healthStatus
39545
+ };
39546
+ });
39537
39547
  }
39538
39548
  async startContainer(id) {
39539
39549
  await this.docker.getContainer(id).start();
@@ -39544,6 +39554,12 @@ var require_DockerClient = __commonJS({
39544
39554
  async restartContainer(id) {
39545
39555
  await this.docker.getContainer(id).restart();
39546
39556
  }
39557
+ async pauseContainer(id) {
39558
+ await this.docker.getContainer(id).pause();
39559
+ }
39560
+ async unpauseContainer(id) {
39561
+ await this.docker.getContainer(id).unpause();
39562
+ }
39547
39563
  async removeContainer(id, force = false) {
39548
39564
  await this.docker.getContainer(id).remove({ force });
39549
39565
  }
@@ -39605,6 +39621,7 @@ var require_DockerClient = __commonJS({
39605
39621
  const memStats = raw.memory_stats;
39606
39622
  const netStats = raw.networks;
39607
39623
  const pidsStats = raw.pids_stats;
39624
+ const blkioStats = raw.blkio_stats;
39608
39625
  const cpuUsage = cpuStats?.cpu_usage?.total_usage ?? 0;
39609
39626
  const systemUsage = cpuStats?.system_cpu_usage ?? 0;
39610
39627
  const numCpus = cpuStats?.cpu_usage?.percpu_usage?.length ?? cpuStats?.online_cpus ?? 1;
@@ -39622,6 +39639,18 @@ var require_DockerClient = __commonJS({
39622
39639
  netTx += iface.tx_bytes ?? 0;
39623
39640
  }
39624
39641
  }
39642
+ let blockRead = 0;
39643
+ let blockWrite = 0;
39644
+ const ioServiceBytes = blkioStats?.io_service_bytes_recursive;
39645
+ if (ioServiceBytes) {
39646
+ for (const entry of ioServiceBytes) {
39647
+ const op = (entry.op || "").toLowerCase();
39648
+ if (op === "read")
39649
+ blockRead += entry.value ?? 0;
39650
+ else if (op === "write")
39651
+ blockWrite += entry.value ?? 0;
39652
+ }
39653
+ }
39625
39654
  return {
39626
39655
  stats: {
39627
39656
  cpuPercent: Math.round(cpuPercent * 100) / 100,
@@ -39630,6 +39659,8 @@ var require_DockerClient = __commonJS({
39630
39659
  memoryPercent: Math.round(memPercent * 100) / 100,
39631
39660
  networkRx: netRx,
39632
39661
  networkTx: netTx,
39662
+ blockRead,
39663
+ blockWrite,
39633
39664
  pids: pidsStats?.current ?? 0,
39634
39665
  timestamp: /* @__PURE__ */ new Date()
39635
39666
  },
@@ -40131,6 +40162,36 @@ var require_StatsCollector = __commonJS({
40131
40162
  return void 0;
40132
40163
  return history.samples[history.samples.length - 1];
40133
40164
  }
40165
+ getNetworkRxRateSeries(containerId) {
40166
+ return this.computeRateSeries(containerId, (s) => s.networkRx);
40167
+ }
40168
+ getNetworkTxRateSeries(containerId) {
40169
+ return this.computeRateSeries(containerId, (s) => s.networkTx);
40170
+ }
40171
+ getBlockReadRateSeries(containerId) {
40172
+ return this.computeRateSeries(containerId, (s) => s.blockRead);
40173
+ }
40174
+ getBlockWriteRateSeries(containerId) {
40175
+ return this.computeRateSeries(containerId, (s) => s.blockWrite);
40176
+ }
40177
+ computeRateSeries(containerId, extractor) {
40178
+ const history = this.histories.get(containerId);
40179
+ if (!history || history.samples.length < 2)
40180
+ return [];
40181
+ const rates = [];
40182
+ for (let i = 1; i < history.samples.length; i++) {
40183
+ const prev = history.samples[i - 1];
40184
+ const curr = history.samples[i];
40185
+ const dt = (curr.timestamp.getTime() - prev.timestamp.getTime()) / 1e3;
40186
+ if (dt > 0) {
40187
+ const delta = extractor(curr) - extractor(prev);
40188
+ rates.push(Math.max(0, delta / dt));
40189
+ } else {
40190
+ rates.push(0);
40191
+ }
40192
+ }
40193
+ return rates;
40194
+ }
40134
40195
  remove(containerId) {
40135
40196
  this.histories.delete(containerId);
40136
40197
  }
@@ -77965,6 +78026,29 @@ var import_react37 = __toESM(require_react(), 1);
77965
78026
  var import_sidekick_docker_shared13 = __toESM(require_dist(), 1);
77966
78027
  import { spawnSync } from "child_process";
77967
78028
 
78029
+ // src/utils/clipboard.ts
78030
+ import { execSync } from "child_process";
78031
+ function copyToClipboard(text) {
78032
+ try {
78033
+ if (process.platform === "darwin") {
78034
+ execSync("pbcopy", { input: text });
78035
+ } else {
78036
+ try {
78037
+ execSync("xclip -selection clipboard", { input: text });
78038
+ } catch {
78039
+ try {
78040
+ execSync("xsel --clipboard --input", { input: text });
78041
+ } catch {
78042
+ execSync("wl-copy", { input: text });
78043
+ }
78044
+ }
78045
+ }
78046
+ return true;
78047
+ } catch {
78048
+ return false;
78049
+ }
78050
+ }
78051
+
77968
78052
  // src/dashboard/DockerState.ts
77969
78053
  var import_sidekick_docker_shared = __toESM(require_dist(), 1);
77970
78054
  var DockerState = class {
@@ -78271,6 +78355,16 @@ function colorizeDetailKey(line) {
78271
78355
  if (!match) return line;
78272
78356
  return ansi.brand(match[1]) + line.substring(match[1].length);
78273
78357
  }
78358
+ function colorizeHealth(status) {
78359
+ switch (status) {
78360
+ case "healthy":
78361
+ return ansi.green(status);
78362
+ case "unhealthy":
78363
+ return ansi.red(status);
78364
+ case "starting":
78365
+ return ansi.yellow(status);
78366
+ }
78367
+ }
78274
78368
  function colorizeState(state) {
78275
78369
  switch (state) {
78276
78370
  case "running":
@@ -78316,6 +78410,8 @@ var ContainersPanel = class {
78316
78410
  onAction;
78317
78411
  onError;
78318
78412
  onExec;
78413
+ onCopyLogs;
78414
+ lastMetrics = null;
78319
78415
  constructor(client, onAction, onError) {
78320
78416
  this.client = client;
78321
78417
  this.onAction = onAction;
@@ -78324,6 +78420,9 @@ var ContainersPanel = class {
78324
78420
  setOnExec(handler) {
78325
78421
  this.onExec = handler;
78326
78422
  }
78423
+ setOnCopyLogs(handler) {
78424
+ this.onCopyLogs = handler;
78425
+ }
78327
78426
  detailTabs = [
78328
78427
  {
78329
78428
  label: "Logs",
@@ -78389,7 +78488,22 @@ var ContainersPanel = class {
78389
78488
  lines.push(` ${coloredSparkline(memSeries, "memory")}`);
78390
78489
  }
78391
78490
  lines.push(
78392
- colorizeDetailKey(`Net: \u25BC ${(0, import_sidekick_docker_shared3.formatBytes)(latest.networkRx)} \u25B2 ${(0, import_sidekick_docker_shared3.formatBytes)(latest.networkTx)}`),
78491
+ colorizeDetailKey(`Net: \u25BC ${(0, import_sidekick_docker_shared3.formatBytes)(latest.networkRx)} \u25B2 ${(0, import_sidekick_docker_shared3.formatBytes)(latest.networkTx)}`)
78492
+ );
78493
+ const rxRates = metrics.statsCollector.getNetworkRxRateSeries(c.id);
78494
+ const txRates = metrics.statsCollector.getNetworkTxRateSeries(c.id);
78495
+ if (rxRates.length > 1) {
78496
+ lines.push(` \u25BC ${coloredSparkline(rxRates, "cpu")} \u25B2 ${coloredSparkline(txRates, "memory")}`);
78497
+ }
78498
+ lines.push(
78499
+ colorizeDetailKey(`IO: R ${(0, import_sidekick_docker_shared3.formatBytes)(latest.blockRead)} W ${(0, import_sidekick_docker_shared3.formatBytes)(latest.blockWrite)}`)
78500
+ );
78501
+ const brRates = metrics.statsCollector.getBlockReadRateSeries(c.id);
78502
+ const bwRates = metrics.statsCollector.getBlockWriteRateSeries(c.id);
78503
+ if (brRates.length > 1) {
78504
+ lines.push(` R ${coloredSparkline(brRates, "cpu")} W ${coloredSparkline(bwRates, "memory")}`);
78505
+ }
78506
+ lines.push(
78393
78507
  colorizeDetailKey(`PIDs: ${latest.pids}`)
78394
78508
  );
78395
78509
  if (metrics.logSeverityTimeSeries.length > 1) {
@@ -78423,6 +78537,7 @@ var ContainersPanel = class {
78423
78537
  sectionHeader("Status"),
78424
78538
  colorizeDetailKey(` State: ${colorizeState(c.state)}`),
78425
78539
  colorizeDetailKey(` Status: ${c.status}`),
78540
+ ...c.healthStatus ? [colorizeDetailKey(` Health: ${colorizeHealth(c.healthStatus)}`)] : [],
78426
78541
  colorizeDetailKey(` Created: ${c.created.toLocaleString()}`),
78427
78542
  "",
78428
78543
  sectionHeader("Network"),
@@ -78453,13 +78568,15 @@ var ContainersPanel = class {
78453
78568
  }
78454
78569
  ];
78455
78570
  getItems(metrics) {
78571
+ this.lastMetrics = metrics;
78456
78572
  return metrics.containers.map((c) => {
78457
78573
  const uptime = compactUptime(c.status);
78458
78574
  const portHint = c.state === "running" && c.ports.length > 0 ? `:${c.ports[0].hostPort || c.ports[0].containerPort}` : "";
78459
- const namePart = portHint ? `${(0, import_sidekick_docker_shared3.truncate)(c.name, 16)} ${portHint}` : (0, import_sidekick_docker_shared3.truncate)(c.name, 20);
78575
+ const healthBadge = c.healthStatus ? ` ${colorizeHealth(c.healthStatus)}` : "";
78576
+ const namePart = portHint ? `${(0, import_sidekick_docker_shared3.truncate)(c.name, 34)} ${portHint}` : (0, import_sidekick_docker_shared3.truncate)(c.name, 38);
78460
78577
  return {
78461
78578
  id: c.id,
78462
- label: `${(0, import_sidekick_docker_shared3.stateIcon)(c.state)} ${namePart}`,
78579
+ label: `${(0, import_sidekick_docker_shared3.stateIcon)(c.state)} ${namePart}${healthBadge}`,
78463
78580
  sortKey: c.state === "running" ? 0 : 1,
78464
78581
  data: c,
78465
78582
  iconColor: (0, import_sidekick_docker_shared3.stateColor)(c.state),
@@ -78497,6 +78614,24 @@ var ContainersPanel = class {
78497
78614
  },
78498
78615
  condition: (item) => item.data.state === "running"
78499
78616
  },
78617
+ {
78618
+ key: "p",
78619
+ label: "Pause",
78620
+ handler: (item) => {
78621
+ const c = item.data;
78622
+ this.client.pauseContainer(c.id).then(() => this.onAction()).catch((e) => this.onError(String(e)));
78623
+ },
78624
+ condition: (item) => item.data.state === "running"
78625
+ },
78626
+ {
78627
+ key: "u",
78628
+ label: "Unpause",
78629
+ handler: (item) => {
78630
+ const c = item.data;
78631
+ this.client.unpauseContainer(c.id).then(() => this.onAction()).catch((e) => this.onError(String(e)));
78632
+ },
78633
+ condition: (item) => item.data.state === "paused"
78634
+ },
78500
78635
  {
78501
78636
  key: "d",
78502
78637
  label: "Remove",
@@ -78515,6 +78650,23 @@ var ContainersPanel = class {
78515
78650
  this.onExec?.(c.id);
78516
78651
  },
78517
78652
  condition: (item) => item.data.state === "running"
78653
+ },
78654
+ {
78655
+ key: "c",
78656
+ label: "Copy Logs",
78657
+ handler: () => {
78658
+ if (!this.lastMetrics || !this.onCopyLogs) return;
78659
+ const logs = this.lastMetrics.selectedContainerLogs;
78660
+ const query = this.lastMetrics.logFilterString;
78661
+ const mode = this.lastMetrics.logFilterMode;
78662
+ let lines;
78663
+ if (query) {
78664
+ lines = logs.filter((l) => (0, import_sidekick_docker_shared4.filterLine)(l.message, query, mode).matched).map((l) => l.message);
78665
+ } else {
78666
+ lines = logs.map((l) => l.message);
78667
+ }
78668
+ this.onCopyLogs(lines.join("\n"));
78669
+ }
78518
78670
  }
78519
78671
  ];
78520
78672
  }
@@ -78600,7 +78752,7 @@ var ServicesPanel = class {
78600
78752
  const icon = (0, import_sidekick_docker_shared3.stateIcon)(service.state);
78601
78753
  items.push({
78602
78754
  id: `service:${project.name}:${service.name}`,
78603
- label: ` ${icon} ${(0, import_sidekick_docker_shared3.truncate)(service.name, 18)}`,
78755
+ label: ` ${icon} ${(0, import_sidekick_docker_shared3.truncate)(service.name, 36)}`,
78604
78756
  sortKey: sortKey++,
78605
78757
  data: { type: "service", service },
78606
78758
  iconColor: (0, import_sidekick_docker_shared3.stateColor)(service.state)
@@ -78709,7 +78861,7 @@ var ImagesPanel = class {
78709
78861
  const icon = img.isDangling ? "\u25CB" : "\u25CF";
78710
78862
  return {
78711
78863
  id: img.id,
78712
- label: `${icon} ${(0, import_sidekick_docker_shared3.truncate)(tag, 20)}`,
78864
+ label: `${icon} ${(0, import_sidekick_docker_shared3.truncate)(tag, 38)}`,
78713
78865
  sortKey: img.isDangling ? 1 : 0,
78714
78866
  data: img,
78715
78867
  iconColor: img.isDangling ? "gray" : "#2B4C7E",
@@ -78780,7 +78932,7 @@ var VolumesPanel = class {
78780
78932
  const icon = vol.isInUse ? "\u25CF" : "\u25CB";
78781
78933
  return {
78782
78934
  id: vol.name,
78783
- label: `${icon} ${(0, import_sidekick_docker_shared3.truncate)(vol.name, 20)}`,
78935
+ label: `${icon} ${(0, import_sidekick_docker_shared3.truncate)(vol.name, 38)}`,
78784
78936
  sortKey: vol.isInUse ? 0 : 1,
78785
78937
  data: vol,
78786
78938
  iconColor: vol.isInUse ? "green" : "gray",
@@ -78862,7 +79014,7 @@ var NetworksPanel = class {
78862
79014
  const countLabel = net.containers.length > 0 ? `${net.containers.length}` : "";
78863
79015
  return {
78864
79016
  id: net.id,
78865
- label: `${icon} ${(0, import_sidekick_docker_shared3.truncate)(net.name, 20)}`,
79017
+ label: `${icon} ${(0, import_sidekick_docker_shared3.truncate)(net.name, 38)}`,
78866
79018
  sortKey: net.isDefault ? 0 : 1,
78867
79019
  data: net,
78868
79020
  iconColor: net.isDefault ? "#2B4C7E" : "gray",
@@ -79207,6 +79359,7 @@ function useWindowedScroll({ totalItems, viewportHeight }) {
79207
79359
 
79208
79360
  // src/dashboard/ink/useKeyboardHandler.ts
79209
79361
  await init_build2();
79362
+ var SORT_FIELDS = ["state", "name", "cpu", "mem", "net", "io", "pids"];
79210
79363
  function executeAction(action, item, dispatch, addToast) {
79211
79364
  if (action.confirm) {
79212
79365
  dispatch({ type: "SET_CONFIRM", action: () => {
@@ -79330,6 +79483,32 @@ function useKeyboardHandler(ctx) {
79330
79483
  }
79331
79484
  return;
79332
79485
  }
79486
+ if (state.overlay === "sort") {
79487
+ if (key.escape) {
79488
+ dispatch({ type: "SET_OVERLAY", overlay: null });
79489
+ return;
79490
+ }
79491
+ if (input === "j" || key.downArrow) {
79492
+ dispatch({ type: "SORT_MENU_NAV", delta: 1 });
79493
+ return;
79494
+ }
79495
+ if (input === "k" || key.upArrow) {
79496
+ dispatch({ type: "SORT_MENU_NAV", delta: -1 });
79497
+ return;
79498
+ }
79499
+ if (input === "R") {
79500
+ dispatch({ type: "TOGGLE_SORT_REVERSE" });
79501
+ addToast(`Sort: ${state.sortReversed ? "ascending" : "descending"}`, "info");
79502
+ return;
79503
+ }
79504
+ if (key.return) {
79505
+ const field = SORT_FIELDS[state.sortMenuIndex];
79506
+ dispatch({ type: "SET_SORT_FIELD", field });
79507
+ addToast(`Sort: ${field}`, "info");
79508
+ return;
79509
+ }
79510
+ return;
79511
+ }
79333
79512
  if (key.escape) {
79334
79513
  if (state.filterString) {
79335
79514
  dispatch({ type: "SET_FILTER", value: "" });
@@ -79362,7 +79541,8 @@ function useKeyboardHandler(ctx) {
79362
79541
  }
79363
79542
  if (input === "z") {
79364
79543
  dispatch({ type: "CYCLE_LAYOUT" });
79365
- addToast(`Layout: ${state.layoutMode === "normal" ? "Expanded" : "Normal"}`, "info");
79544
+ const nextMode = state.layoutMode === "normal" ? "Wide" : state.layoutMode === "wide" ? "Expanded" : "Normal";
79545
+ addToast(`Layout: ${nextMode}`, "info");
79366
79546
  return;
79367
79547
  }
79368
79548
  if (input === "/") {
@@ -79375,6 +79555,20 @@ function useKeyboardHandler(ctx) {
79375
79555
  return;
79376
79556
  }
79377
79557
  }
79558
+ if (input === "a" && panel.id === "containers") {
79559
+ dispatch({ type: "TOGGLE_SHOW_ALL" });
79560
+ addToast(state.showAllContainers ? "Running only" : "Show all", "info");
79561
+ return;
79562
+ }
79563
+ if (input === "o" && panel.id === "containers") {
79564
+ dispatch({ type: "SET_OVERLAY", overlay: "sort" });
79565
+ return;
79566
+ }
79567
+ if (input === "R" && panel.id === "containers") {
79568
+ dispatch({ type: "TOGGLE_SORT_REVERSE" });
79569
+ addToast(`Sort: ${state.sortReversed ? "ascending" : "descending"}`, "info");
79570
+ return;
79571
+ }
79378
79572
  if (input === "x") {
79379
79573
  if (selectedItem && panelActions.length > 0) {
79380
79574
  dispatch({ type: "SET_OVERLAY", overlay: "context-menu" });
@@ -79573,7 +79767,7 @@ function TabBar({ panels, activeIndex, layoutMode, phrase, panelCounts }) {
79573
79767
  ] }, panel.id);
79574
79768
  }),
79575
79769
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box_default, { flexGrow: 1, marginLeft: 1, children: phrase && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: "gray", wrap: "truncate", children: phrase }) }),
79576
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: layoutMode === "expanded" ? "#2B4C7E" : "gray", children: `z: ${layoutMode === "expanded" ? "Expanded" : "Normal"} \u25B8` })
79770
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: layoutMode !== "normal" ? "#2B4C7E" : "gray", children: `z: ${layoutMode === "expanded" ? "Expanded" : layoutMode === "wide" ? "Wide" : "Normal"} \u25B8` })
79577
79771
  ] });
79578
79772
  }
79579
79773
 
@@ -79746,9 +79940,12 @@ var GLOBAL_BINDINGS = [
79746
79940
  { key: "g/G", label: "Jump to first / last" },
79747
79941
  { key: "Tab", label: "Toggle focus" },
79748
79942
  { key: "[/]", label: "Cycle detail tabs" },
79749
- { key: "z", label: "Toggle expanded layout" },
79943
+ { key: "z", label: "Cycle layout (Normal/Wide/Expanded)" },
79750
79944
  { key: "/", label: "Filter items" },
79751
79945
  { key: "x", label: "Actions menu" },
79946
+ { key: "a", label: "Toggle all/running (Containers)" },
79947
+ { key: "o", label: "Sort menu (Containers)" },
79948
+ { key: "R", label: "Reverse sort (Containers)" },
79752
79949
  { key: "V", label: "Version info" },
79753
79950
  { key: "?", label: "This help" },
79754
79951
  { key: "q", label: "Quit" }
@@ -80087,6 +80284,52 @@ function VersionOverlay({ version: version2 }) {
80087
80284
  ] });
80088
80285
  }
80089
80286
 
80287
+ // src/dashboard/ink/SortOverlay.tsx
80288
+ await init_build2();
80289
+ var import_jsx_runtime16 = __toESM(require_jsx_runtime(), 1);
80290
+ var SORT_OPTIONS = [
80291
+ { field: "state", label: "State (running first)" },
80292
+ { field: "name", label: "Name" },
80293
+ { field: "cpu", label: "CPU %" },
80294
+ { field: "mem", label: "Memory %" },
80295
+ { field: "net", label: "Network I/O" },
80296
+ { field: "io", label: "Block I/O" },
80297
+ { field: "pids", label: "PIDs" }
80298
+ ];
80299
+ function SortOverlay({ selectedIndex, currentField, reversed }) {
80300
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
80301
+ Box_default,
80302
+ {
80303
+ position: "absolute",
80304
+ marginTop: 2,
80305
+ marginLeft: 2,
80306
+ flexDirection: "column",
80307
+ borderStyle: "single",
80308
+ borderColor: "#2B4C7E",
80309
+ paddingX: 1,
80310
+ children: [
80311
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Text, { bold: true, color: "#2B4C7E", children: "\u2195 Sort by" }),
80312
+ SORT_OPTIONS.map((opt, i) => {
80313
+ const isSelected = i === selectedIndex;
80314
+ const isCurrent = opt.field === currentField;
80315
+ const indicator = isCurrent ? reversed ? " \u25B2" : " \u25BC" : "";
80316
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Box_default, { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
80317
+ Text,
80318
+ {
80319
+ color: isSelected ? "#2B4C7E" : isCurrent ? "yellow" : "white",
80320
+ bold: isSelected,
80321
+ inverse: isSelected,
80322
+ children: ` ${opt.label}${indicator} `
80323
+ }
80324
+ ) }, opt.field);
80325
+ }),
80326
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Text, { children: "" }),
80327
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Text, { color: "gray", dimColor: true, children: "j/k select Enter apply R reverse Esc close" })
80328
+ ]
80329
+ }
80330
+ );
80331
+ }
80332
+
80090
80333
  // src/dashboard/ink/Dashboard.tsx
80091
80334
  var import_sidekick_docker_shared12 = __toESM(require_dist(), 1);
80092
80335
 
@@ -80145,8 +80388,10 @@ var ExecManager = class {
80145
80388
  };
80146
80389
 
80147
80390
  // src/dashboard/ink/Dashboard.tsx
80148
- var import_jsx_runtime16 = __toESM(require_jsx_runtime(), 1);
80391
+ var import_jsx_runtime17 = __toESM(require_jsx_runtime(), 1);
80392
+ var SORT_FIELDS2 = ["state", "name", "cpu", "mem", "net", "io", "pids"];
80149
80393
  var SIDE_PANEL_WIDTH = 28;
80394
+ var SIDE_PANEL_WIDTH_WIDE = 42;
80150
80395
  var MIN_SCREEN_WIDTH = 60;
80151
80396
  var MIN_SCREEN_HEIGHT = 15;
80152
80397
  var RESERVED_UI_ROWS = 5;
@@ -80174,7 +80419,7 @@ function reducer(state, action) {
80174
80419
  return { ...state, detailTabIndex: next, detailScrollOffset: 0 };
80175
80420
  }
80176
80421
  case "CYCLE_LAYOUT": {
80177
- const next = state.layoutMode === "normal" ? "expanded" : "normal";
80422
+ const next = state.layoutMode === "normal" ? "wide" : state.layoutMode === "wide" ? "expanded" : "normal";
80178
80423
  return { ...state, layoutMode: next, focusTarget: next === "expanded" ? "detail" : state.focusTarget };
80179
80424
  }
80180
80425
  case "TOGGLE_FOCUS":
@@ -80231,6 +80476,16 @@ function reducer(state, action) {
80231
80476
  return { ...state, logFilterString: action.value };
80232
80477
  case "TOGGLE_LOG_FILTER_MODE":
80233
80478
  return { ...state, logFilterMode: state.logFilterMode === "exact" ? "fuzzy" : "exact" };
80479
+ case "TOGGLE_SHOW_ALL":
80480
+ return { ...state, showAllContainers: !state.showAllContainers, selectedItemIndex: 0 };
80481
+ case "SET_SORT_FIELD":
80482
+ return { ...state, sortField: action.field, overlay: null };
80483
+ case "TOGGLE_SORT_REVERSE":
80484
+ return { ...state, sortReversed: !state.sortReversed };
80485
+ case "SORT_MENU_NAV": {
80486
+ const next = (state.sortMenuIndex + action.delta + SORT_FIELDS2.length) % SORT_FIELDS2.length;
80487
+ return { ...state, sortMenuIndex: next };
80488
+ }
80234
80489
  default:
80235
80490
  return state;
80236
80491
  }
@@ -80252,7 +80507,11 @@ var initialState = {
80252
80507
  execContainerId: null,
80253
80508
  execContainerName: "",
80254
80509
  logFilterString: "",
80255
- logFilterMode: "exact"
80510
+ logFilterMode: "exact",
80511
+ showAllContainers: true,
80512
+ sortField: "state",
80513
+ sortReversed: false,
80514
+ sortMenuIndex: 0
80256
80515
  };
80257
80516
  function Dashboard({ panels, metrics, onSelectionChange, execTriggerRef, onExecFallback }) {
80258
80517
  const [state, dispatch] = (0, import_react36.useReducer)(reducer, initialState);
@@ -80261,17 +80520,19 @@ function Dashboard({ panels, metrics, onSelectionChange, execTriggerRef, onExecF
80261
80520
  const execManagerRef = (0, import_react36.useRef)(null);
80262
80521
  const [phrase, setPhrase] = import_react36.default.useState(() => (0, import_sidekick_docker_shared12.getRandomPhrase)());
80263
80522
  const phraseTimerRef = (0, import_react36.useRef)(null);
80264
- const rotatePhrase = (0, import_react36.useCallback)(() => {
80523
+ const rotatePhraseRef = (0, import_react36.useRef)(void 0);
80524
+ rotatePhraseRef.current = () => {
80265
80525
  setPhrase((0, import_sidekick_docker_shared12.getRandomPhrase)());
80266
80526
  if (phraseTimerRef.current) clearTimeout(phraseTimerRef.current);
80267
- phraseTimerRef.current = setTimeout(rotatePhrase, 7e3);
80268
- }, []);
80527
+ phraseTimerRef.current = setTimeout(() => rotatePhraseRef.current?.(), 7e3);
80528
+ };
80529
+ const rotatePhrase = (0, import_react36.useCallback)(() => rotatePhraseRef.current?.(), []);
80269
80530
  (0, import_react36.useEffect)(() => {
80270
- phraseTimerRef.current = setTimeout(rotatePhrase, 7e3);
80531
+ phraseTimerRef.current = setTimeout(() => rotatePhraseRef.current?.(), 7e3);
80271
80532
  return () => {
80272
80533
  if (phraseTimerRef.current) clearTimeout(phraseTimerRef.current);
80273
80534
  };
80274
- }, [rotatePhrase]);
80535
+ }, []);
80275
80536
  (0, import_react36.useEffect)(() => {
80276
80537
  if (!execTriggerRef) return;
80277
80538
  execTriggerRef.current = (containerId, containerName) => {
@@ -80336,11 +80597,17 @@ function Dashboard({ panels, metrics, onSelectionChange, execTriggerRef, onExecF
80336
80597
  }, []);
80337
80598
  const panel = panels[state.activePanelIndex];
80338
80599
  const tooSmall = columns < MIN_SCREEN_WIDTH || rows < MIN_SCREEN_HEIGHT;
80339
- const sideWidth = state.layoutMode === "expanded" ? 0 : SIDE_PANEL_WIDTH;
80600
+ const sideWidth = state.layoutMode === "expanded" ? 0 : state.layoutMode === "wide" ? SIDE_PANEL_WIDTH_WIDE : SIDE_PANEL_WIDTH;
80340
80601
  const allItems = panel.getItems(metrics);
80341
80602
  const totalItemCount = allItems.length;
80342
80603
  const currentItems = (() => {
80343
80604
  let items = allItems;
80605
+ if (panel.id === "containers" && !state.showAllContainers) {
80606
+ items = items.filter((it) => {
80607
+ const c = it.data;
80608
+ return c.state === "running" || c.state === "paused";
80609
+ });
80610
+ }
80344
80611
  if (state.filterString) {
80345
80612
  const f = state.filterString.toLowerCase();
80346
80613
  items = items.filter((it) => {
@@ -80348,7 +80615,47 @@ function Dashboard({ panels, metrics, onSelectionChange, execTriggerRef, onExecF
80348
80615
  return text.toLowerCase().includes(f);
80349
80616
  });
80350
80617
  }
80351
- items.sort((a, b) => a.sortKey - b.sortKey);
80618
+ if (panel.id === "containers" && state.sortField !== "state") {
80619
+ const dir = state.sortReversed ? -1 : 1;
80620
+ items.sort((a, b) => {
80621
+ const ca = a.data;
80622
+ const cb = b.data;
80623
+ switch (state.sortField) {
80624
+ case "name":
80625
+ return dir * ca.name.localeCompare(cb.name);
80626
+ case "cpu": {
80627
+ const sa = metrics.statsCollector.getLatest(ca.id)?.cpuPercent ?? 0;
80628
+ const sb = metrics.statsCollector.getLatest(cb.id)?.cpuPercent ?? 0;
80629
+ return dir * (sb - sa);
80630
+ }
80631
+ case "mem": {
80632
+ const sa = metrics.statsCollector.getLatest(ca.id)?.memoryPercent ?? 0;
80633
+ const sb = metrics.statsCollector.getLatest(cb.id)?.memoryPercent ?? 0;
80634
+ return dir * (sb - sa);
80635
+ }
80636
+ case "net": {
80637
+ const sa = metrics.statsCollector.getLatest(ca.id);
80638
+ const sb = metrics.statsCollector.getLatest(cb.id);
80639
+ return dir * ((sb?.networkRx ?? 0) + (sb?.networkTx ?? 0) - ((sa?.networkRx ?? 0) + (sa?.networkTx ?? 0)));
80640
+ }
80641
+ case "io": {
80642
+ const sa = metrics.statsCollector.getLatest(ca.id);
80643
+ const sb = metrics.statsCollector.getLatest(cb.id);
80644
+ return dir * ((sb?.blockRead ?? 0) + (sb?.blockWrite ?? 0) - ((sa?.blockRead ?? 0) + (sa?.blockWrite ?? 0)));
80645
+ }
80646
+ case "pids": {
80647
+ const sa = metrics.statsCollector.getLatest(ca.id)?.pids ?? 0;
80648
+ const sb = metrics.statsCollector.getLatest(cb.id)?.pids ?? 0;
80649
+ return dir * (sb - sa);
80650
+ }
80651
+ default:
80652
+ return a.sortKey - b.sortKey;
80653
+ }
80654
+ });
80655
+ } else {
80656
+ const dir = state.sortReversed ? -1 : 1;
80657
+ items.sort((a, b) => dir * (a.sortKey - b.sortKey));
80658
+ }
80352
80659
  return items;
80353
80660
  })();
80354
80661
  const clampedSelection = Math.min(state.selectedItemIndex, Math.max(0, currentItems.length - 1));
@@ -80437,14 +80744,14 @@ function Dashboard({ panels, metrics, onSelectionChange, execTriggerRef, onExecF
80437
80744
  rotatePhrase
80438
80745
  });
80439
80746
  if (tooSmall) {
80440
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(TooSmallOverlay, { columns, rows });
80747
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(TooSmallOverlay, { columns, rows });
80441
80748
  }
80442
80749
  const showNormalLayout = state.overlay !== "help" && state.overlay !== "exec" && state.overlay !== "version";
80443
80750
  const panelActionHints = applicableActions.map((a) => ({ key: a.key, label: a.label, destructive: !!a.confirm }));
80444
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(MouseProvider, { onMouse: handleMouse, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(Box_default, { flexDirection: "column", height: rows, width: columns, children: [
80445
- showNormalLayout && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(TabBar, { panels, activeIndex: state.activePanelIndex, layoutMode: state.layoutMode, phrase, panelCounts }),
80446
- showNormalLayout && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(Box_default, { flexGrow: 1, flexDirection: "row", children: [
80447
- sideWidth > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
80751
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(MouseProvider, { onMouse: handleMouse, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(Box_default, { flexDirection: "column", height: rows, width: columns, children: [
80752
+ showNormalLayout && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(TabBar, { panels, activeIndex: state.activePanelIndex, layoutMode: state.layoutMode, phrase, panelCounts }),
80753
+ showNormalLayout && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(Box_default, { flexGrow: 1, flexDirection: "row", children: [
80754
+ sideWidth > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
80448
80755
  SideList,
80449
80756
  {
80450
80757
  items: currentItems,
@@ -80460,9 +80767,9 @@ function Dashboard({ panels, metrics, onSelectionChange, execTriggerRef, onExecF
80460
80767
  runningCount: panel.id === "containers" ? runningCount : void 0
80461
80768
  }
80462
80769
  ),
80463
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(Box_default, { flexDirection: "column", flexGrow: 1, children: [
80464
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(DetailTabBar, { tabs: detailTabs, activeIndex: state.detailTabIndex }),
80465
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
80770
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(Box_default, { flexDirection: "column", flexGrow: 1, children: [
80771
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(DetailTabBar, { tabs: detailTabs, activeIndex: state.detailTabIndex }),
80772
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
80466
80773
  DetailPane,
80467
80774
  {
80468
80775
  content: detailContent,
@@ -80473,16 +80780,16 @@ function Dashboard({ panels, metrics, onSelectionChange, execTriggerRef, onExecF
80473
80780
  )
80474
80781
  ] })
80475
80782
  ] }),
80476
- state.overlay === "help" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(HelpOverlay, { panels, activePanelIndex: state.activePanelIndex, version: "0.1.3" }),
80477
- state.overlay === "version" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(VersionOverlay, { version: "0.1.3" }),
80478
- state.overlay === "exec" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
80783
+ state.overlay === "help" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(HelpOverlay, { panels, activePanelIndex: state.activePanelIndex, version: "0.1.5" }),
80784
+ state.overlay === "version" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(VersionOverlay, { version: "0.1.5" }),
80785
+ state.overlay === "exec" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
80479
80786
  ExecOverlay,
80480
80787
  {
80481
80788
  containerName: state.execContainerName,
80482
80789
  outputLines: state.execOutputLines
80483
80790
  }
80484
80791
  ),
80485
- state.overlay !== "exec" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
80792
+ state.overlay !== "exec" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
80486
80793
  StatusBar,
80487
80794
  {
80488
80795
  daemonConnected: metrics.daemonConnected,
@@ -80491,14 +80798,14 @@ function Dashboard({ panels, metrics, onSelectionChange, execTriggerRef, onExecF
80491
80798
  filterString: state.filterString,
80492
80799
  containerCount: metrics.containers.length,
80493
80800
  runningCount,
80494
- version: "0.1.3",
80801
+ version: "0.1.5",
80495
80802
  matchCount: state.filterString ? currentItems.length : void 0,
80496
80803
  totalCount: state.filterString ? totalItemCount : void 0,
80497
80804
  lastRefresh: metrics.lastRefresh
80498
80805
  }
80499
80806
  ),
80500
- state.overlay === "context-menu" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ContextMenuOverlay, { actions: contextActions, selectedIndex: state.contextMenuIndex }),
80501
- state.overlay === "filter" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
80807
+ state.overlay === "context-menu" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ContextMenuOverlay, { actions: contextActions, selectedIndex: state.contextMenuIndex }),
80808
+ state.overlay === "filter" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
80502
80809
  FilterOverlay,
80503
80810
  {
80504
80811
  filterString: state.filterString,
@@ -80507,14 +80814,15 @@ function Dashboard({ panels, metrics, onSelectionChange, execTriggerRef, onExecF
80507
80814
  panelTitle: panel.title
80508
80815
  }
80509
80816
  ),
80510
- state.overlay === "log-filter" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
80817
+ state.overlay === "log-filter" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
80511
80818
  LogFilterOverlay,
80512
80819
  {
80513
80820
  filterString: state.logFilterString,
80514
80821
  filterMode: state.logFilterMode
80515
80822
  }
80516
80823
  ),
80517
- state.overlay === "confirm" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
80824
+ state.overlay === "sort" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SortOverlay, { selectedIndex: state.sortMenuIndex, currentField: state.sortField, reversed: state.sortReversed }),
80825
+ state.overlay === "confirm" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
80518
80826
  ConfirmOverlay,
80519
80827
  {
80520
80828
  message: state.confirmMessage,
@@ -80525,7 +80833,7 @@ function Dashboard({ panels, metrics, onSelectionChange, execTriggerRef, onExecF
80525
80833
  onCancel: () => dispatch({ type: "SET_CONFIRM", action: null, message: "" })
80526
80834
  }
80527
80835
  ),
80528
- state.toasts.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ToastNotification, { toast: state.toasts[state.toasts.length - 1] })
80836
+ state.toasts.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ToastNotification, { toast: state.toasts[state.toasts.length - 1] })
80529
80837
  ] }) });
80530
80838
  }
80531
80839
 
@@ -80650,6 +80958,9 @@ async function dashboardAction(_opts, cmd) {
80650
80958
  onExecFallback(containerId);
80651
80959
  }
80652
80960
  });
80961
+ containersPanel.setOnCopyLogs((text) => {
80962
+ copyToClipboard(text);
80963
+ });
80653
80964
  let renderTimer = null;
80654
80965
  function getEnrichedMetrics() {
80655
80966
  const m = state.getMetrics();
@@ -80773,7 +81084,7 @@ async function logsAction(container, opts) {
80773
81084
 
80774
81085
  // src/cli.ts
80775
81086
  var program2 = new Command();
80776
- program2.name("sidekick-docker").description("Docker management TUI dashboard").version("0.1.3").option("--socket <path>", "Docker socket path").action(async (_opts, cmd) => {
81087
+ program2.name("sidekick-docker").description("Docker management TUI dashboard").version("0.1.5").option("--socket <path>", "Docker socket path").action(async (_opts, cmd) => {
80777
81088
  await dashboardAction(_opts, cmd);
80778
81089
  });
80779
81090
  program2.command("ps").description("List containers").option("-a, --all", "Show all containers (default: running only)", false).action(async (opts) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidekick-docker",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Docker management TUI dashboard",
5
5
  "author": "Cesar Andres Lopez <cesarandreslopez@gmail.com>",
6
6
  "repository": {