Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions README.ko-KR.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

<p align="center">
<img src="https://img.shields.io/badge/Go-1.25+-00ADD8?style=for-the-badge&logo=go&logoColor=white" alt="Go Version">
<a href="https://amqcli.pages.dev/"><img src="https://img.shields.io/badge/Website-amqcli.pages.dev-4A90D9?style=for-the-badge&logo=cloudflarepages&logoColor=white" alt="Website"></a>
<img src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge" alt="License: MIT">
<img src="https://img.shields.io/badge/Platform-Linux%20%7C%20macOS%20%7C%20Windows-lightgrey?style=for-the-badge" alt="Platform">
<img src="https://img.shields.io/badge/Arch-amd64%20%7C%20arm64-blueviolet?style=for-the-badge" alt="Architecture">
Expand Down Expand Up @@ -76,10 +77,10 @@ flowchart LR

| Broker Type | 버전 | TUI 대시보드 (Jolokia) | 메시지 송수신 (AMQP/STOMP) | 비고 |
| :--- | :--- | :---: | :---: | :--- |
| **ActiveMQ Classic** | 5.x ~ 6.x | 🟢 완벽 지원 | 🟢 완벽 지원 | `amqcli`의 주 타겟 브로커입니다. |
| **ActiveMQ Artemis** | 2.x ~ | ❌ 미지원 | 🟢 지원 | Artemis는 JMX MBean 구조가 완전히 달라 대시보드는 렌더링되지 않으나, 프로토콜 기반 송수신은 가능합니다. |
| **ActiveMQ Classic** | 5.x ~ 6.x | 🟢 완벽 지원 | 🟢 완벽 지원 | Native JMX/Jolokia MBean 기반 관리 지원 |
| **ActiveMQ Artemis** | 2.x ~ 3.x | 🚧 작업 중 (WIP) | 🚧 작업 중 (WIP) | 현재 개발 작업이 진행 중입니다. |

> *참고: `amqcli`의 TUI 대시보드가 보여주는 Jolokia(JMX) 메트릭은 브로커 메모리 상의 실시간 뷰(Runtime View)입니다. KahaDB 등 디스크에 저장된 영속성(Persistence) 데이터 전체와 미세한 시차가 발생할 수 있습니다.*
> *참고: `amqcli`는 브로커에 연결할 때 대상이 Classic인지 Artemis인지 자동으로 탐색하여 최적의 어댑터를 연결합니다.*


---
Expand All @@ -100,12 +101,12 @@ brew install amqcli

**macOS / Linux (Shell)**
```bash
curl -fsSL https://raw.githubusercontent.com/xvlet/amqcli/master/install.sh | sh
curl -fsSL https://amqcli.pages.dev/install.sh | sh
```

**Windows (PowerShell)**
```powershell
powershell -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/xvlet/amqcli/master/install.ps1 | iex"
powershell -ExecutionPolicy Bypass -c "irm https://amqcli.pages.dev/install.ps1 | iex"
```

### 3. Go 명령어 사용 (go install)
Expand Down Expand Up @@ -146,14 +147,16 @@ refresh_interval: 1s
encoding: utf-8
environments:
dev:
protocol: "stomp" # 또는 "amqp"
broker_type: "auto" # "auto", "classic", 또는 "artemis" (기본값: auto)
protocol: "stomp" # 또는 "amqp"
host: "${MQ_HOST:-127.0.0.1}"
stomp_port: "61613" # 선택 사항 (기본값: 61613)
web_port: "8161" # 선택 사항 (기본값: 8161)
username: "${MQ_USER:-admin}"
password: "${MQ_PASS:-admin}"
readonly: false
prod:
broker_type: "auto"
protocol: "amqp"
host: "10.0.0.5"
amqp_port: "5672" # 선택 사항 (기본값: 5672)
Expand Down
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

<p align="center">
<img src="https://img.shields.io/badge/Go-1.25+-00ADD8?style=for-the-badge&logo=go&logoColor=white" alt="Go Version">
<a href="https://amqcli.pages.dev/"><img src="https://img.shields.io/badge/Website-amqcli.pages.dev-4A90D9?style=for-the-badge&logo=cloudflarepages&logoColor=white" alt="Website"></a>
<img src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge" alt="License: MIT">
<img src="https://img.shields.io/badge/Platform-Linux%20%7C%20macOS%20%7C%20Windows-lightgrey?style=for-the-badge" alt="Platform">
<img src="https://img.shields.io/badge/Arch-amd64%20%7C%20arm64-blueviolet?style=for-the-badge" alt="Architecture">
Expand Down Expand Up @@ -76,10 +77,10 @@ flowchart LR

| Broker Type | Versions | TUI Dashboard (Jolokia) | Messaging (AMQP/STOMP) | Notes |
| :--- | :--- | :---: | :---: | :--- |
| **ActiveMQ Classic** | 5.x ~ 6.x | 🟢 Fully Supported | 🟢 Fully Supported | The primary target broker for `amqcli`. |
| **ActiveMQ Artemis** | 2.x ~ | ❌ Not Supported | 🟢 Supported | Artemis uses a completely different JMX MBean structure. While the TUI dashboard cannot render metrics, protocol-based message sending still functions. |
| **ActiveMQ Classic** | 5.x ~ 6.x | 🟢 Fully Supported | 🟢 Fully Supported | Native JMX/Jolokia MBean management. |
| **ActiveMQ Artemis** | 2.x ~ 3.x | 🚧 Work In Progress | 🚧 Work In Progress | Development in progress. |

> *Note: The Jolokia (JMX) metrics displayed in the TUI represent the broker's real-time runtime memory view, which may have slight discrepancies with the fully persisted KahaDB disk state.*
> *Note: `amqcli` automatically detects whether the target broker is Classic or Artemis. The Jolokia (JMX) metrics displayed in the TUI represent the broker's real-time runtime memory view.*


---
Expand All @@ -100,12 +101,12 @@ The easiest way to install the latest release is by using the provided installat

**macOS / Linux (Shell)**
```bash
curl -fsSL https://raw.githubusercontent.com/xvlet/amqcli/master/install.sh | sh
curl -fsSL https://amqcli.pages.dev/install.sh | sh
```

**Windows (PowerShell)**
```powershell
powershell -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/xvlet/amqcli/master/install.ps1 | iex"
powershell -ExecutionPolicy Bypass -c "irm https://amqcli.pages.dev/install.ps1 | iex"
```

### 3. Using Go (go install)
Expand Down Expand Up @@ -146,14 +147,16 @@ refresh_interval: 1s
encoding: utf-8
environments:
dev:
protocol: "stomp" # or "amqp"
broker_type: "auto" # "auto", "classic", or "artemis" (default: auto)
protocol: "stomp" # "stomp" or "amqp"
host: "${MQ_HOST:-127.0.0.1}"
stomp_port: "61613" # optional (default: 61613)
web_port: "8161" # optional (default: 8161)
username: "${MQ_USER:-admin}"
password: "${MQ_PASS:-admin}"
readonly: false
prod:
broker_type: "auto"
protocol: "amqp"
host: "10.0.0.5"
amqp_port: "5672" # optional (default: 5672)
Expand Down
10 changes: 5 additions & 5 deletions adapter/inbound/ui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,11 @@ func NewAppModel(uc usecase.UseCase, interval time.Duration, host string, env st

// 1. Queue Table
qCols := []table.Column{
{Title: "Name", Width: 26},
{Title: fmt.Sprintf("%10s", "Pending"), Width: 10},
{Title: fmt.Sprintf("%10s", "Consumers"), Width: 10},
{Title: fmt.Sprintf("%10s", "Enqueued"), Width: 10},
{Title: fmt.Sprintf("%10s", "Dequeued"), Width: 10},
{Title: "Name", Width: 46},
{Title: fmt.Sprintf("%12s", "Pending"), Width: 12},
{Title: fmt.Sprintf("%12s", "Consumers"), Width: 12},
{Title: fmt.Sprintf("%12s", "Enqueued"), Width: 12},
{Title: fmt.Sprintf("%12s", "Dequeued"), Width: 12},
}
qTable := table.New(table.WithColumns(qCols), table.WithFocused(true))

Expand Down
20 changes: 1 addition & 19 deletions adapter/inbound/ui/tui_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -779,26 +779,8 @@ func (m *AppModel) updateConnections(msg tea.Msg) (tea.Model, tea.Cmd) {
}

func (m *AppModel) updateQueueTableColumns() {
// Name width is preserved from existing configuration to avoid shrinking unexpectedly
qNameW := 26
if len(m.queueTable.Columns()) > 0 {
qNameW = m.queueTable.Columns()[0].Width
}
qCols := []table.Column{
{Title: "Name", Width: qNameW},
{Title: fmt.Sprintf("%12s", "Pending"), Width: 12},
{Title: fmt.Sprintf("%12s", "Consumers"), Width: 12},
{Title: fmt.Sprintf("%12s", "Enqueued"), Width: 12},
{Title: fmt.Sprintf("%12s", "Dequeued"), Width: 12},
}
if m.viewStats {
qCols = append(qCols,
table.Column{Title: "Memory", Width: 30},
table.Column{Title: "Disk", Width: 15},
)
}
m.queueTable.SetRows([]table.Row{}) // Temporarily clear rows to prevent panic during SetColumns
m.queueTable.SetColumns(qCols)
m.recalculateTableWidths()
}

func (m *AppModel) buildQueueRows(queues []domain.Queue) []table.Row {
Expand Down
158 changes: 91 additions & 67 deletions adapter/inbound/ui/tui_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,44 +14,69 @@ func (m *AppModel) recalculateTableWidths() {
return
}

// Calculate inner width for safety checks (Margin(2)+Border(2)+Padding(2)=6 offset)
contentWidth := m.width - 10
// Box inner usable width: Width(m.width-6) with Border(2) and Padding(2) gives m.width - 10
innerUsableWidth := m.width - 10
if innerUsableWidth < 40 {
innerUsableWidth = 40
}

// 1. Queue Table Resizing
// Use original fixed widths, only shrink Name if terminal is too narrow
qNameW := 26
if contentWidth < 105 {
qNameW = contentWidth - 70
// Max cap at 46 so UUID queue names (36 chars) fit completely without creating huge empty gaps in fullscreen
if !m.viewStats {
// 5 columns: Name + 4 fixed metrics (12 chars each = 48) + 5*2 cell padding (10) = 58 fixed overhead
qNameW := innerUsableWidth - 58 - 2
if qNameW > 46 {
qNameW = 46
}
if qNameW < 10 {
qNameW = 10
}
}
qCols := []table.Column{
{Title: "Name", Width: qNameW},
{Title: fmt.Sprintf("%12s", "Pending"), Width: 12},
{Title: fmt.Sprintf("%12s", "Consumers"), Width: 12},
{Title: fmt.Sprintf("%12s", "Enqueued"), Width: 12},
{Title: fmt.Sprintf("%12s", "Dequeued"), Width: 12},
}
if m.viewStats {
qCols = append(qCols,
table.Column{Title: "Memory", Width: 30},
table.Column{Title: "Disk", Width: 15},
)
}
m.queueTable.SetColumns(qCols)

// 2. Message Table Resizing
// Use original fixed widths where possible, shrink ID/Correlation proportionally if needed
mIdW := 46
mCorrW := 36
if contentWidth < 164 {
slack := contentWidth - 82
if slack < 20 {
slack = 20
m.queueTable.SetColumns([]table.Column{
{Title: "Name", Width: qNameW},
{Title: fmt.Sprintf("%12s", "Pending"), Width: 12},
{Title: fmt.Sprintf("%12s", "Consumers"), Width: 12},
{Title: fmt.Sprintf("%12s", "Enqueued"), Width: 12},
{Title: fmt.Sprintf("%12s", "Dequeued"), Width: 12},
})
} else {
// 7 columns: Name + 4 metrics (48) + Memory (26) + Disk (12) = 86 + 7*2 padding (14) = 100 fixed overhead
qNameW := innerUsableWidth - 100 - 2
if qNameW > 46 {
qNameW = 46
}
if qNameW < 10 {
qNameW = 10
}
mIdW = int(float64(slack) * 0.56)
mCorrW = slack - mIdW
m.queueTable.SetColumns([]table.Column{
{Title: "Name", Width: qNameW},
{Title: fmt.Sprintf("%12s", "Pending"), Width: 12},
{Title: fmt.Sprintf("%12s", "Consumers"), Width: 12},
{Title: fmt.Sprintf("%12s", "Enqueued"), Width: 12},
{Title: fmt.Sprintf("%12s", "Dequeued"), Width: 12},
{Title: "Memory", Width: 26},
{Title: "Disk", Width: 12},
})
}

// 2. Message Table Resizing (8 columns)
// 8*2 padding (16) + SEQ(5) + Persistence(12) + Priority(8) + Redelivered(12) + Timestamp(24) + Action(10) = 87 fixed overhead
msgSlack := innerUsableWidth - 87 - 2
if msgSlack < 20 {
msgSlack = 20
}
mIdW := int(float64(msgSlack) * 0.55)
mCorrW := msgSlack - mIdW
if mIdW > 46 {
mIdW = 46
}
if mCorrW > 36 {
mCorrW = 36
}
if mIdW < 10 {
mIdW = 10
}
if mCorrW < 10 {
mCorrW = 10
}

m.msgTable.SetColumns([]table.Column{
Expand All @@ -61,55 +86,54 @@ func (m *AppModel) recalculateTableWidths() {
{Title: "Persistence", Width: 12},
{Title: "Priority", Width: 8},
{Title: "Redelivered", Width: 12},
{Title: "Timestamp", Width: 30},
{Title: "Action", Width: 15},
{Title: "Timestamp", Width: 24},
{Title: "Action", Width: 10},
})

// 3. Connections Table Resizing
cNameW := 40
cAddrW := 30
if contentWidth < 90 {
cAddrW = contentWidth - 60
if cAddrW < 10 {
cAddrW = 10
}
cNameW = contentWidth - cAddrW - 20
if cNameW < 10 {
cNameW = 10
}
// 3. Connections Table Resizing (4 columns)
// 4*2 padding (8) + Active(10) + Slow(10) = 28 fixed overhead
connSlack := innerUsableWidth - 28 - 2
if connSlack < 20 {
connSlack = 20
}
cNameW := int(float64(connSlack) * 0.55)
cAddrW := connSlack - cNameW
if cNameW > 46 {
cNameW = 46
}
if cAddrW > 32 {
cAddrW = 32
}
if cNameW < 10 {
cNameW = 10
}
if cAddrW < 10 {
cAddrW = 10
}

m.connectionsTable.SetColumns([]table.Column{
{Title: "Name", Width: cNameW},
{Title: "Remote Address", Width: cAddrW},
{Title: "Active", Width: 10},
{Title: "Slow", Width: 10},
})

// 4. Consumers Table Resizing
conPidW := 10
conAddrW := 25
conClientW := 40
conDeqW := 10
conUptimeW := 15

if contentWidth < 100 {
// narrower terminal, shrink ClientID proportionally
conClientW = contentWidth - 60
if conClientW < 10 {
conClientW = 10
}
conAddrW = contentWidth - conClientW - 35
if conAddrW < 10 {
conAddrW = 10
}
// 4. Consumers Table Resizing (5 columns)
// 5*2 padding (10) + PID(10) + Remote Address(22) + Dequeues(10) + Uptime(12) = 64 fixed overhead
conClientW := innerUsableWidth - 64 - 2
if conClientW > 46 {
conClientW = 46
}
if conClientW < 10 {
conClientW = 10
}

m.consumersTable.SetColumns([]table.Column{
{Title: "PID", Width: conPidW},
{Title: "Remote Address", Width: conAddrW},
{Title: "PID", Width: 10},
{Title: "Remote Address", Width: 22},
{Title: "Client ID", Width: conClientW},
{Title: "Dequeues", Width: conDeqW},
{Title: "Uptime", Width: conUptimeW},
{Title: "Dequeues", Width: 10},
{Title: "Uptime", Width: 12},
})
}

Expand Down
Loading
Loading