项目文件夹

文件
wehub-resource-sync 36b3af2e3d
PR Check / Code Quality: Format (push) Failing after 1s
PR Check / Code Quality: Lint (darwin) (push) Failing after 0s
PR Check / Code Quality: Lint (freebsd) (push) Failing after 1s
PR Check / Code Quality: Lint (windows) (push) Failing after 1s
PR Check / Code Quality: Lint (linux) (push) Failing after 1s
PR Check / Security: Vulnerability Scan (push) Failing after 0s
Update Documentation / update-docs (push) Failing after 2s
PR Check / Code Quality: Vendor (push) Failing after 1s
PR Check / Code Quality: Coverage (push) Failing after 0s
PR Check / Tests: Unit (macos-latest) (push) Has been cancelled
PR Check / Tests: Unit (ubuntu-24.04) (push) Has been cancelled
PR Check / Tests: Unit (ubuntu-24.04-arm) (push) Has been cancelled
PR Check / Tests: Unit (windows-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:22:06 +08:00

449 行
13 KiB
Go

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
package tui
import (
"fmt"
"os"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/pranshuparmar/witr/internal/output"
)
func (m MainModel) View() string {
if m.quitting {
return ""
}
outerStyle := baseStyle.
Width(m.width-2).
Height(m.height-2).
Padding(0, 1)
switch m.state {
case stateList:
return m.viewList(outerStyle)
case stateDetail:
if m.selectedDetail == nil && m.selectedContainer == nil {
return m.viewDetailLoading(outerStyle)
}
if m.selectedContainer != nil {
return m.viewContainerDetail(outerStyle)
}
return m.viewProcessDetail(outerStyle)
}
return "Unknown state"
}
func (m MainModel) viewList(outerStyle lipgloss.Style) string {
status := "Mode: Navigation (Press / to search)"
if m.statusMsg != "" {
status = errorStyle.Render(m.statusMsg)
}
inputView := m.input.View()
switch m.activeTab {
case tabPorts:
if m.portInput.Focused() {
status = "Mode: Searching (↑↓ to navigate, Esc/Enter to stop)"
}
inputView = m.portInput.View()
case tabContainers:
if m.containerInput.Focused() {
status = "Mode: Searching (↑↓ to navigate, Esc/Enter to stop)"
}
inputView = m.containerInput.View()
case tabLocks:
if m.lockInput.Focused() {
status = "Mode: Searching (↑↓ to navigate, Esc/Enter to stop)"
}
inputView = m.lockInput.View()
default:
if m.input.Focused() {
status = "Mode: Searching (↑↓ to navigate, Esc/Enter to stop)"
}
}
activeBorderColor := colorAccent
dimBorderColor := colorBorderDim
treeBorderColor := dimBorderColor
treeHeaderColor := colorHeaderDim
if m.listFocus == focusSide {
treeBorderColor = activeBorderColor
treeHeaderColor = activeBorderColor
}
treeContainerStyle := paneDividerStyle.
BorderForeground(treeBorderColor).
Height(m.table.Height())
treeHeader := "Details"
selected := m.table.SelectedRow()
if len(selected) > 0 {
treeHeader = fmt.Sprintf("PID %s", strings.TrimSpace(selected[0]))
}
if !m.treeViewport.AtTop() && !m.treeViewport.AtBottom() {
treeHeader += " ↕"
} else if !m.treeViewport.AtTop() {
treeHeader += " ↑"
} else if !m.treeViewport.AtBottom() {
treeHeader += " ↓"
}
treeHeaderStyle := tableHeaderStyle.
Width(m.treeViewport.Width).
Foreground(treeHeaderColor).
BorderForeground(treeBorderColor)
s := cachedTableStyles
if m.listFocus == focusMain {
s.Header = tableHeaderStyle.BorderForeground(activeBorderColor)
} else {
s.Header = tableHeaderStyle.BorderForeground(dimBorderColor)
}
m.table.SetStyles(s)
availableWidth := m.width - 6
processListPaneWidth := int(float64(availableWidth) * listPaneRatio)
if processListPaneWidth < 10 {
processListPaneWidth = 10
}
mainContent := lipgloss.JoinHorizontal(lipgloss.Top,
lipgloss.NewStyle().Width(processListPaneWidth).Render(m.table.View()),
treeContainerStyle.Render(
lipgloss.JoinVertical(lipgloss.Left,
treeHeaderStyle.Render(treeHeader),
paddedStyle.Render(m.treeViewport.View()),
),
),
)
if m.activeTab == tabContainers {
s1 := cachedTableStyles
s1.Header = s.Header
m.containerTable.SetStyles(s1)
mainContent = lipgloss.NewStyle().Width(m.width - 4).Render(m.containerTable.View())
}
if m.activeTab == tabLocks {
s1 := cachedTableStyles
s1.Header = s.Header
m.lockTable.SetStyles(s1)
mainContent = lipgloss.NewStyle().Width(m.width - 4).Render(m.lockTable.View())
}
if m.activeTab == tabPorts {
sideBorderColor := dimBorderColor
sideHeaderColor := colorHeaderDim
if m.listFocus == focusSide {
sideBorderColor = activeBorderColor
sideHeaderColor = activeBorderColor
}
// Reuse cached styles — only header border differs per table
s1 := cachedTableStyles
s1.Header = s.Header // same focus state as main table
m.portTable.SetStyles(s1)
s2 := cachedTableStyles
if m.listFocus == focusSide {
s2.Header = tableHeaderStyle.BorderForeground(activeBorderColor)
} else {
s2.Header = tableHeaderStyle.BorderForeground(dimBorderColor)
}
m.portDetailTable.SetStyles(s2)
detailContainerStyle := detailDividerStyle.
BorderForeground(sideBorderColor).
Height(m.portTable.Height())
detailHeader := "Attached Processes"
availableWidth := m.width - 6
portPaneWidth := int(float64(availableWidth) * portPaneRatio)
headerWidth := availableWidth - portPaneWidth - 3
detailHeaderStyle := tableHeaderStyle.
Width(headerWidth).
Foreground(sideHeaderColor).
BorderForeground(sideBorderColor)
mainContent = lipgloss.JoinHorizontal(lipgloss.Top,
lipgloss.NewStyle().Width(portPaneWidth).Render(m.portTable.View()),
detailContainerStyle.Render(
lipgloss.JoinVertical(lipgloss.Left,
detailHeaderStyle.Render(detailHeader),
m.portDetailTable.View(),
),
),
)
}
helpText := fmt.Sprintf("Total: %d | Enter: Detail | p/n/u/c/m/t: Sort | Esc/q: Quit | Tab: Focus | Up/Down: Scroll", len(m.filtered))
switch m.activeTab {
case tabPorts:
filterStatus := "LISTEN"
if m.showAllPorts {
filterStatus = "ALL"
}
helpText = fmt.Sprintf("Total: %d [%s] | p/t/n/s: Sort | a: Toggle All | Esc/q: Quit | Tab: Focus | Up/Down: Scroll", len(m.portTable.Rows()), filterStatus)
case tabContainers:
helpText = fmt.Sprintf("Total: %d | Enter: Detail | i/n/r/g/s: Sort | /: Search | Esc/q: Quit | Up/Down: Scroll", len(m.containerTable.Rows()))
case tabLocks:
suffix := ""
if os.Geteuid() != 0 {
suffix = " | (use sudo for full paths)"
}
mode := "LOCKED"
if m.showAllFiles {
mode = "OPEN"
}
shown := len(m.lockTable.Rows())
total := len(m.filteredLocks)
countText := fmt.Sprintf("Total: %d", total)
if shown < total {
countText = fmt.Sprintf("%d of %d", shown, total)
}
helpText = fmt.Sprintf("%s [%s] | Enter: Detail | a: Toggle Open Files | p/n/t/m/f: Sort | /: Search | Esc/q: Quit | Up/Down: Scroll%s", countText, mode, suffix)
}
footerContent := helpText
if m.version != "" {
gap := m.width - 6 - lipgloss.Width(helpText) - lipgloss.Width(m.version)
if gap > 0 {
footerContent = helpText + strings.Repeat(" ", gap) + m.version
}
}
processesTab := inactiveTabStyle.Render("1. Processes")
portsTab := inactiveTabStyle.Render("2. Ports")
containersTab := inactiveTabStyle.Render("3. Containers")
locksTab := inactiveTabStyle.Render("4. Locks")
switch m.activeTab {
case tabProcesses:
processesTab = activeTabStyle.Render("1. Processes")
case tabPorts:
portsTab = activeTabStyle.Render("2. Ports")
case tabContainers:
containersTab = activeTabStyle.Render("3. Containers")
case tabLocks:
locksTab = activeTabStyle.Render("4. Locks")
}
headerSegs := []string{
titleStyle.Render("witr"),
processesTab,
portsTab,
containersTab,
}
if locksTabEnabled {
headerSegs = append(headerSegs, locksTab)
}
header := lipgloss.JoinHorizontal(lipgloss.Top, headerSegs...)
return outerStyle.Render(
lipgloss.JoinVertical(lipgloss.Left,
header,
spacerStyle.Render(""),
statusBarStyle.Render(status),
statusBarStyle.Render(inputView),
mainContent,
spacerStyle.Render(""),
footerStyle.Width(m.width-4).Render(footerContent),
),
)
}
func (m MainModel) viewDetailLoading(outerStyle lipgloss.Style) string {
helpText := "Esc/q: Back"
footerContent := helpText
if m.version != "" {
gap := m.width - 6 - lipgloss.Width(helpText) - lipgloss.Width(m.version)
if gap > 0 {
footerContent = helpText + strings.Repeat(" ", gap) + m.version
}
}
return outerStyle.Render(
lipgloss.JoinVertical(lipgloss.Left,
lipgloss.JoinHorizontal(lipgloss.Center, titleStyle.Render("witr")),
spacerStyle.Render(""),
lipgloss.NewStyle().Width(m.width-4).Height(m.height-7).Render("Loading details..."),
spacerStyle.Render(""),
footerStyle.Width(m.width-4).Render(footerContent),
),
)
}
func (m MainModel) viewContainerDetail(outerStyle lipgloss.Style) string {
activeBorderColor := colorAccent
detailHeader := tableHeaderStyle.
BorderForeground(activeBorderColor).
Foreground(activeBorderColor)
title := "Container Detail"
if !m.viewport.AtTop() && !m.viewport.AtBottom() {
title += " ↕"
} else if !m.viewport.AtTop() {
title += " ↑"
} else if !m.viewport.AtBottom() {
title += " ↓"
}
headerComponents := []string{titleStyle.Render("witr")}
if id := output.ShortContainerID(m.selectedContainer.ID); id != "" {
headerComponents = append(headerComponents, pidStyle.Render("ID "+id))
}
helpText := "Esc/q: Back | Up/Down: Scroll"
footerContent := helpText
if m.version != "" {
gap := m.width - 6 - lipgloss.Width(helpText) - lipgloss.Width(m.version)
if gap > 0 {
footerContent = helpText + strings.Repeat(" ", gap) + m.version
}
}
// Match the structure used for process detail: a single Width-
// and Height-pinned pane that contains the title row plus the
// scrollable viewport. The pane absorbs any content/scroll
// variation so the surrounding layout stays put.
paneWidth := m.width - 4
if paneWidth < 1 {
paneWidth = 1
}
contentPane := lipgloss.NewStyle().
Width(paneWidth).
Height(m.viewport.Height + 2).
Render(lipgloss.JoinVertical(lipgloss.Left,
detailHeader.Width(m.viewport.Width).Render(title),
paddedStyle.Render(m.viewport.View()),
))
return outerStyle.Render(
lipgloss.JoinVertical(lipgloss.Left,
lipgloss.JoinHorizontal(lipgloss.Center, headerComponents...),
spacerStyle.Render(""),
contentPane,
spacerStyle.Render(""),
footerStyle.Width(m.width-4).Render(footerContent),
),
)
}
func (m MainModel) viewProcessDetail(outerStyle lipgloss.Style) string {
availableWidth := m.width - 6
if availableWidth < 0 {
availableWidth = 0
}
detailWidth := int(float64(availableWidth) * detailPaneRatio)
envWidth := availableWidth - detailWidth
envContainerStyle := envPanelStyle.
Width(envWidth).
Height(m.viewport.Height + 2)
detailHeader := tableHeaderStyle
envHeader := tableHeaderStyle
activeBorderColor := colorAccent
dimColor := colorHeaderDim
dimBorderColor := colorBorderDim
if m.detailFocus == focusDetail {
detailHeader = detailHeader.BorderForeground(activeBorderColor).Foreground(activeBorderColor)
envHeader = envHeader.BorderForeground(dimBorderColor).Foreground(dimColor)
envContainerStyle = envContainerStyle.BorderForeground(dimBorderColor)
} else {
detailHeader = detailHeader.BorderForeground(dimBorderColor).Foreground(dimColor)
envHeader = envHeader.BorderForeground(activeBorderColor).Foreground(activeBorderColor)
envContainerStyle = envContainerStyle.BorderForeground(activeBorderColor)
}
detailTitle := "Process Detail"
if !m.viewport.AtTop() && !m.viewport.AtBottom() {
detailTitle += " ↕"
} else if !m.viewport.AtTop() {
detailTitle += " ↑"
} else if !m.viewport.AtBottom() {
detailTitle += " ↓"
}
envTitle := "Environment Variables"
if !m.envViewport.AtTop() && !m.envViewport.AtBottom() {
envTitle += " ↕"
} else if !m.envViewport.AtTop() {
envTitle += " ↑"
} else if !m.envViewport.AtBottom() {
envTitle += " ↓"
}
splitContent := lipgloss.JoinHorizontal(lipgloss.Top,
lipgloss.NewStyle().Width(detailWidth).Render(
lipgloss.JoinVertical(lipgloss.Left,
detailHeader.Width(m.viewport.Width).Render(detailTitle),
paddedStyle.Render(m.viewport.View()),
),
),
envContainerStyle.Render(
lipgloss.JoinVertical(lipgloss.Left,
envHeader.Width(m.envViewport.Width).Render(envTitle),
paddedStyle.Render(m.envViewport.View()),
),
),
)
headerComponents := []string{
titleStyle.Render("witr"),
}
if m.selectedDetail != nil {
headerComponents = append(headerComponents, pidStyle.Render(fmt.Sprintf("PID %d", m.selectedDetail.Process.PID)))
}
var helpText string
pid := 0
if m.selectedDetail != nil {
pid = m.selectedDetail.Process.PID
}
switch {
case m.actionMenuOpen:
helpText = actionMenuStyle.Render("Esc/q: cancel | Actions: [k]ill [t]erm [p]ause [r]esume [n]ice")
case m.pendingAction == actionKill:
helpText = confirmStyle.Render(fmt.Sprintf("Kill PID %d? [y]es / [n]o", pid))
case m.pendingAction == actionTerm:
helpText = confirmStyle.Render(fmt.Sprintf("Terminate PID %d? [y]es / [n]o", pid))
case m.pendingAction == actionPause:
helpText = confirmStyle.Render(fmt.Sprintf("Pause PID %d? [y]es / [n]o", pid))
case m.pendingAction == actionResume:
helpText = confirmStyle.Render(fmt.Sprintf("Resume PID %d? [y]es / [n]o", pid))
case m.pendingAction == actionRenice:
helpText = confirmStyle.Render(fmt.Sprintf("Nice value for PID %d (20…19): ", pid)) + m.reniceInput.View()
case m.statusMsg != "":
helpText = errorStyle.Render(m.statusMsg)
default:
if actionsSupported {
helpText = "a: Actions | Esc/q: Back | Tab: Focus | Up/Down: Scroll"
} else {
helpText = "Esc/q: Back | Tab: Focus | Up/Down: Scroll"
}
}
footerContent := helpText
if m.version != "" && !m.actionMenuOpen && m.pendingAction == actionNone && m.statusMsg == "" {
gap := m.width - 6 - lipgloss.Width(helpText) - lipgloss.Width(m.version)
if gap > 0 {
footerContent = helpText + strings.Repeat(" ", gap) + m.version
}
}
return outerStyle.Render(
lipgloss.JoinVertical(lipgloss.Left,
lipgloss.JoinHorizontal(lipgloss.Center, headerComponents...),
spacerStyle.Render(""),
splitContent,
spacerStyle.Render(""),
footerStyle.Width(m.width-4).Render(footerContent),
),
)
}