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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ It lets you browse, search, connect to, edit, and organize the hosts defined in
### Fast Navigation
- Fuzzy search by alias, IP, user, or tag
- Press `Enter` to connect to the selected host immediately
- See currently running local SSH sessions in a read-only panel
- See currently running local SSH sessions in an auto-refreshing read-only panel
- Organize hosts with tags such as `prod`, `dev`, and `test`
- Sort by alias or last connection time, with reversible order

Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ lazyssh 是一个运行在终端中的交互式 SSH 管理器,灵感来自 `la
### 快速导航
- 支持按别名、IP、用户或标签进行模糊搜索
- 选中主机后按回车即可直接 SSH 登录
- 只读展示当前本机正在运行的 SSH 会话
- 在自动刷新的只读面板中展示当前本机正在运行的 SSH 会话
- 支持用标签组织主机,例如 `prod`、`dev`、`test`
- 支持按别名或最近连接时间排序,并可反转排序方向

Expand Down
5 changes: 4 additions & 1 deletion internal/adapters/ui/active_sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,11 @@ func formatActiveSessionLine(session domain.ActiveSession) string {
colorText(CurrentTheme.MetaText, fmt.Sprintf("%d", session.PID)),
colorText(CurrentTheme.AliasText, tview.Escape(alias)),
}
if session.Destination != "" && session.Destination != alias {
parts = append(parts, colorText(CurrentTheme.DimText, tview.Escape(session.Destination)))
}
if len(session.Forwards) > 0 {
parts = append(parts, colorText(CurrentTheme.HostText, strings.Join(session.Forwards, ", ")))
parts = append(parts, colorText(CurrentTheme.HostText, "fwd "+strings.Join(session.Forwards, ", ")))
}
return strings.Join(parts, " ")
}
15 changes: 15 additions & 0 deletions internal/adapters/ui/active_sessions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,18 @@ func TestActiveSessionsSetSessionsShowsEmptyState(t *testing.T) {
t.Fatalf("text = %q, want empty state", text)
}
}

func TestFormatActiveSessionLineShowsDestinationAndForwards(t *testing.T) {
line := formatActiveSessionLine(domain.ActiveSession{
PID: 12345,
Alias: "example.com",
Destination: "ops@example.com",
Forwards: []string{"8080:localhost:80"},
})

for _, want := range []string{"12345", "example.com", "ops@example.com", "fwd 8080:localhost:80"} {
if !strings.Contains(line, want) {
t.Fatalf("line = %q, want it to contain %q", line, want)
}
}
}
56 changes: 56 additions & 0 deletions internal/adapters/ui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package ui

import (
"strings"
"time"

"go.uber.org/zap"

Expand Down Expand Up @@ -52,6 +53,8 @@ type tui struct {
sortMode SortMode
themeMode ThemeMode
searchVisible bool

activeSessionsRefreshStop chan struct{}
}

type mainUIState struct {
Expand All @@ -68,6 +71,8 @@ const (
focusDetails = "details"
)

const activeSessionsRefreshInterval = 10 * time.Second

func NewTUI(logger *zap.SugaredLogger, ss ports.ServerService, version, commit string) App {
return &tui{
logger: logger,
Expand All @@ -91,6 +96,8 @@ func (t *tui) Run() error {
t.loadPreferences().initializeTheme().buildComponents().buildLayout().bindEvents()
t.loadInitialData()
t.app.SetRoot(t.root, true)
t.startActiveSessionsRefresh()
defer t.stopActiveSessionsRefresh()
t.logger.Infow("starting TUI application", "version", t.version, "commit", t.commit)
if err := t.app.Run(); err != nil {
t.logger.Errorw("application run error", "error", err)
Expand Down Expand Up @@ -202,6 +209,55 @@ func (t *tui) loadActiveSessions() {
t.sessions.SetSessions(sessions)
}

func (t *tui) startActiveSessionsRefresh() {
if t.app == nil || t.serverService == nil || t.activeSessionsRefreshStop != nil {
return
}

stop := make(chan struct{})
t.activeSessionsRefreshStop = stop

go func() {
ticker := time.NewTicker(activeSessionsRefreshInterval)
defer ticker.Stop()

for {
select {
case <-ticker.C:
t.refreshActiveSessionsFromBackground()
case <-stop:
return
}
}
}()
}

func (t *tui) stopActiveSessionsRefresh() {
if t.activeSessionsRefreshStop == nil {
return
}
close(t.activeSessionsRefreshStop)
t.activeSessionsRefreshStop = nil
}

func (t *tui) refreshActiveSessionsFromBackground() {
sessions, err := t.serverService.ListActiveSessions()
if t.app == nil {
return
}

t.app.QueueUpdateDraw(func() {
if t.sessions == nil {
return
}
if err != nil {
t.sessions.SetText(colorText(CurrentTheme.Error, "Failed to load active sessions"))
return
}
t.sessions.SetSessions(sessions)
})
}

func (t *tui) updateListTitle() {
if t.serverList != nil {
t.serverList.SetTitle(" 1 Servers — Sort: " + t.sortMode.String() + " ")
Expand Down
Loading