From 87c7f40d2dfdad50887036bbeaf77968993c3a5c Mon Sep 17 00:00:00 2001 From: Adem Baccara <71262172+Adembc@users.noreply.github.com> Date: Fri, 19 Sep 2025 12:31:49 +0100 Subject: [PATCH 1/2] ci: add action to enforce semantic PR titles and run tests (#55) --- .github/workflows/go.yml | 39 ++++++++++++++++++++++ .github/workflows/semantic-prs.yml | 43 +++++++++++++++++++++++++ .golangci.yml | 9 +++++- README.md | 29 +++++++++++++++++ internal/adapters/ui/validation_test.go | 25 ++++++++++++++ 5 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/go.yml create mode 100644 .github/workflows/semantic-prs.yml diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..d12a903 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,39 @@ +name: Go Build and Test + +on: + pull_request: + branches: + - main + types: + - edited + - opened + - reopened + - synchronize + push: + branches: + - main + +permissions: + contents: read + pull-requests: read + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + cache: true + + - name: Run make build + run: make build + + - name: Run make test + run: make test \ No newline at end of file diff --git a/.github/workflows/semantic-prs.yml b/.github/workflows/semantic-prs.yml new file mode 100644 index 0000000..78d04fe --- /dev/null +++ b/.github/workflows/semantic-prs.yml @@ -0,0 +1,43 @@ +name: Semantic PRs + +on: + pull_request: + types: + - edited + - opened + - reopened + - synchronize + +permissions: + pull-requests: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.number }} + cancel-in-progress: true + +jobs: + validate_title: + name: Validate Title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5.5.3 + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + with: + types: | + fix + feat + improve + refactor + revert + test + ci + docs + chore + + scopes: | + ui + cli + config + parser + requireScope: false \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index e44d3ac..b723bc9 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -14,7 +14,14 @@ issues: - dupl - lll - + # exclude some linters for the test directory and test files + - path: test/.*|.*_test\.go + linters: + - dupl + - errcheck + - goconst + - gocyclo + - gosec linters: disable-all: true diff --git a/README.md b/README.md index 8b05377..4b50d7f 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,35 @@ Contributions are welcome! We love seeing the community make Lazyssh better πŸš€ +### Semantic Pull Requests + +This repository enforces semantic PR titles via an automated GitHub Action. Please format your PR title as: + +- type(scope): short descriptive subject +Notes: +- Scope is optional and should be one of: ui, cli, config, parser. + +Allowed types in this repo: +- feat: a new feature +- fix: a bug fix +- improve: quality or UX improvements that are not a refactor or perf +- refactor: code change that neither fixes a bug nor adds a feature +- docs: documentation only changes +- test: adding or refactoring tests +- ci: CI/CD or automation changes +- chore: maintenance tasks, dependency bumps, non-code infra +- revert: reverts a previous commit + +Examples: +- feat(ui): add server pinning and sorting options +- fix(parser): handle comments at end of Host blocks +- improve(cli): show friendly error when ssh binary missing +- refactor(config): simplify backup rotation logic +- docs: add installation instructions for Homebrew +- ci: cache Go toolchain and dependencies + +Tip: If your PR touches multiple areas, pick the most relevant scope or omit the scope. + --- ## ⭐ Support diff --git a/internal/adapters/ui/validation_test.go b/internal/adapters/ui/validation_test.go index 9968372..de33639 100644 --- a/internal/adapters/ui/validation_test.go +++ b/internal/adapters/ui/validation_test.go @@ -15,6 +15,8 @@ package ui import ( + "os" + "path/filepath" "testing" ) @@ -134,6 +136,29 @@ func TestValidateBindAddress(t *testing.T) { } func TestValidateKeyPaths(t *testing.T) { + // Prepare an isolated HOME with a mock .ssh folder and key files + oldHome := os.Getenv("HOME") + t.Cleanup(func() { + _ = os.Setenv("HOME", oldHome) + }) + + tempHome := t.TempDir() + sshDir := filepath.Join(tempHome, ".ssh") + if err := os.MkdirAll(sshDir, 0o755); err != nil { + t.Fatalf("failed to create temp .ssh dir: %v", err) + } + + shouldExistFiles := []string{"id_rsa", "id_ed25519"} + for _, name := range shouldExistFiles { + p := filepath.Join(sshDir, name) + if err := os.WriteFile(p, []byte("test"), 0o644); err != nil { + t.Fatalf("failed to create mock key file %s: %v", p, err) + } + } + if err := os.Setenv("HOME", tempHome); err != nil { + t.Fatalf("failed to set HOME: %v", err) + } + tests := []struct { name string keys string From 0a14cb83c62b4b58ed57b9bc86651a02b07f9c81 Mon Sep 17 00:00:00 2001 From: cp90 <153345481+cp90-pixel@users.noreply.github.com> Date: Fri, 19 Sep 2025 20:29:24 -0400 Subject: [PATCH 2/2] Add support for hiding servers --- README.md | 1 + .../adapters/data/ssh_config_file/mapper.go | 2 + .../data/ssh_config_file/metadata_manager.go | 15 +++++ .../ssh_config_file/ssh_config_file_repo.go | 5 ++ internal/adapters/ui/handlers.go | 63 ++++++++++++++++--- internal/adapters/ui/hint_bar.go | 2 +- internal/adapters/ui/server_details.go | 10 ++- internal/adapters/ui/status_bar.go | 2 +- internal/adapters/ui/tui.go | 26 +++++++- internal/adapters/ui/utils.go | 34 +++++++++- internal/adapters/ui/utils_test.go | 30 +++++++++ internal/core/domain/server.go | 1 + internal/core/ports/repositories.go | 1 + internal/core/ports/services.go | 1 + internal/core/services/server_service.go | 9 +++ 15 files changed, 183 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 4b50d7f..9f6ae22 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ With lazyssh, you can quickly navigate, connect, manage, and transfer files betw - ✏ Edit existing server entries directly from the UI with a tabbed interface. - πŸ—‘ Delete server entries safely. - πŸ“Œ Pin / unpin servers to keep favorites at the top. +- πŸ‘» Hide servers you rarely use and toggle their visibility on demand. - πŸ“ Ping server to check status. ### Quick Server Navigation diff --git a/internal/adapters/data/ssh_config_file/mapper.go b/internal/adapters/data/ssh_config_file/mapper.go index f8a31f4..61c5d0d 100644 --- a/internal/adapters/data/ssh_config_file/mapper.go +++ b/internal/adapters/data/ssh_config_file/mapper.go @@ -295,10 +295,12 @@ func (r *Repository) mapDebugConfig(server *domain.Server, key, value string) bo func (r *Repository) mergeMetadata(servers []domain.Server, metadata map[string]ServerMetadata) []domain.Server { for i, server := range servers { servers[i].LastSeen = time.Time{} + servers[i].Hidden = false if meta, exists := metadata[server.Alias]; exists { servers[i].Tags = meta.Tags servers[i].SSHCount = meta.SSHCount + servers[i].Hidden = meta.Hidden if meta.LastSeen != "" { if lastSeen, err := time.Parse(time.RFC3339, meta.LastSeen); err == nil { diff --git a/internal/adapters/data/ssh_config_file/metadata_manager.go b/internal/adapters/data/ssh_config_file/metadata_manager.go index a4e7be7..cb3b473 100644 --- a/internal/adapters/data/ssh_config_file/metadata_manager.go +++ b/internal/adapters/data/ssh_config_file/metadata_manager.go @@ -30,6 +30,7 @@ type ServerMetadata struct { LastSeen string `json:"last_seen,omitempty"` PinnedAt string `json:"pinned_at,omitempty"` SSHCount int `json:"ssh_count,omitempty"` + Hidden bool `json:"hidden,omitempty"` } type metadataManager struct { @@ -149,6 +150,20 @@ func (m *metadataManager) setPinned(alias string, pinned bool) error { return m.saveAll(metadata) } +func (m *metadataManager) setHidden(alias string, hidden bool) error { + metadata, err := m.loadAll() + if err != nil { + m.logger.Errorw("failed to load metadata in setHidden", "path", m.filePath, "alias", alias, "hidden", hidden, "error", err) + return fmt.Errorf("load metadata: %w", err) + } + + meta := metadata[alias] + meta.Hidden = hidden + metadata[alias] = meta + + return m.saveAll(metadata) +} + func (m *metadataManager) recordSSH(alias string) error { metadata, err := m.loadAll() if err != nil { diff --git a/internal/adapters/data/ssh_config_file/ssh_config_file_repo.go b/internal/adapters/data/ssh_config_file/ssh_config_file_repo.go index 37e8004..51ee4f4 100644 --- a/internal/adapters/data/ssh_config_file/ssh_config_file_repo.go +++ b/internal/adapters/data/ssh_config_file/ssh_config_file_repo.go @@ -160,6 +160,11 @@ func (r *Repository) SetPinned(alias string, pinned bool) error { return r.metadataManager.setPinned(alias, pinned) } +// SetHidden sets or unsets the hidden status of a server. +func (r *Repository) SetHidden(alias string, hidden bool) error { + return r.metadataManager.setHidden(alias, hidden) +} + // RecordSSH increments the SSH access count and updates the last seen timestamp for a server. func (r *Repository) RecordSSH(alias string) error { return r.metadataManager.recordSSH(alias) diff --git a/internal/adapters/ui/handlers.go b/internal/adapters/ui/handlers.go index 7b1becd..61820c5 100644 --- a/internal/adapters/ui/handlers.go +++ b/internal/adapters/ui/handlers.go @@ -54,6 +54,12 @@ func (t *tui) handleGlobalKeys(event *tcell.EventKey) *tcell.EventKey { case 'p': t.handleServerPin() return nil + case 'h': + t.handleServerHideToggle() + return nil + case 'H': + t.handleToggleHiddenVisibility() + return nil case 's': t.handleSortToggle() return nil @@ -100,6 +106,33 @@ func (t *tui) handleServerPin() { } } +func (t *tui) handleServerHideToggle() { + if server, ok := t.serverList.GetSelectedServer(); ok { + hidden := !server.Hidden + if err := t.serverService.SetHidden(server.Alias, hidden); err != nil { + t.showStatusTempColor(fmt.Sprintf("Failed to update hidden state: %v", err), "#FF6B6B") + return + } + if hidden { + t.showStatusTemp("Hidden " + server.Alias) + } else { + t.showStatusTemp("Unhid " + server.Alias) + } + t.refreshServerList() + } +} + +func (t *tui) handleToggleHiddenVisibility() { + t.showHidden = !t.showHidden + state := "off" + if t.showHidden { + state = "on" + } + t.updateListTitle() + t.refreshServerList() + t.showStatusTemp("Hidden hosts: " + state) +} + func (t *tui) handleSortToggle() { t.sortMode = t.sortMode.ToggleField() t.showStatusTemp("Sort: " + t.sortMode.String()) @@ -155,8 +188,11 @@ func (t *tui) handleNavigateUp() { } func (t *tui) handleSearchInput(query string) { - filtered, _ := t.serverService.ListServers(query) - sortServersForUI(filtered, t.sortMode) + filtered, _, err := t.fetchServers(query, t.showHidden, t.sortMode) + if err != nil { + t.showStatusTempColor(fmt.Sprintf("Search failed: %v", err), "#FF6B6B") + return + } t.serverList.UpdateServers(filtered) if len(filtered) == 0 { t.details.ShowEmpty() @@ -271,15 +307,14 @@ func (t *tui) handleRefreshBackground() { t.showStatusTemp("Refreshing…") - go func(prevIdx int, q string) { - servers, err := t.serverService.ListServers(q) + go func(prevIdx int, q string, includeHidden bool, sortMode SortMode) { + servers, hiddenCount, err := t.fetchServers(q, includeHidden, sortMode) if err != nil { t.app.QueueUpdateDraw(func() { t.showStatusTempColor(fmt.Sprintf("Refresh failed: %v", err), "#FF6B6B") }) return } - sortServersForUI(servers, t.sortMode) t.app.QueueUpdateDraw(func() { t.serverList.UpdateServers(servers) // Try to restore selection if still valid @@ -289,9 +324,13 @@ func (t *tui) handleRefreshBackground() { t.details.UpdateServer(srv) } } - t.showStatusTemp(fmt.Sprintf("Refreshed %d servers", len(servers))) + message := fmt.Sprintf("Refreshed %d servers", len(servers)) + if !t.showHidden && hiddenCount > 0 { + message = fmt.Sprintf("Refreshed %d servers (%d hidden)", len(servers), hiddenCount) + } + t.showStatusTemp(message) }) - }(currentIdx, query) + }(currentIdx, query, t.showHidden, t.sortMode) } // ============================================================================= @@ -397,9 +436,15 @@ func (t *tui) refreshServerList() { if t.searchVisible { query = t.searchBar.InputField.GetText() } - filtered, _ := t.serverService.ListServers(query) - sortServersForUI(filtered, t.sortMode) + filtered, _, err := t.fetchServers(query, t.showHidden, t.sortMode) + if err != nil { + t.showStatusTempColor(fmt.Sprintf("Refresh failed: %v", err), "#FF6B6B") + return + } t.serverList.UpdateServers(filtered) + if len(filtered) == 0 { + t.details.ShowEmpty() + } } func (t *tui) returnToMain() { diff --git a/internal/adapters/ui/hint_bar.go b/internal/adapters/ui/hint_bar.go index de94973..f74c5f8 100644 --- a/internal/adapters/ui/hint_bar.go +++ b/internal/adapters/ui/hint_bar.go @@ -22,6 +22,6 @@ import ( func NewHintBar() *tview.TextView { hint := tview.NewTextView().SetDynamicColors(true) hint.SetBackgroundColor(tcell.Color233) - hint.SetText("[#BBBBBB]Press [::b]/[-:-:b] to search… β€’ ↑↓ Navigate β€’ Enter SSH β€’ c Copy SSH β€’ g Ping β€’ r Refresh β€’ a Add β€’ e Edit β€’ t Tags β€’ d Delete β€’ p Pin/Unpin β€’ s Sort[-]") + hint.SetText("[#BBBBBB]Press [::b]/[-:-:b] to search… β€’ ↑↓ Navigate β€’ Enter SSH β€’ c Copy SSH β€’ g Ping β€’ r Refresh β€’ a Add β€’ e Edit β€’ t Tags β€’ d Delete β€’ p Pin/Unpin β€’ h Hide β€’ H Hidden view β€’ s Sort[-]") return hint } diff --git a/internal/adapters/ui/server_details.go b/internal/adapters/ui/server_details.go index 8e0a634..cdc9c5c 100644 --- a/internal/adapters/ui/server_details.go +++ b/internal/adapters/ui/server_details.go @@ -68,6 +68,10 @@ func (sd *ServerDetails) UpdateServer(server domain.Server) { if server.PinnedAt.IsZero() { pinnedStr = "false" } + hiddenStr := "false" + if server.Hidden { + hiddenStr = "true" + } tagsText := renderTagChips(server.Tags) // Basic information @@ -83,9 +87,9 @@ func (sd *ServerDetails) UpdateServer(server domain.Server) { } text := fmt.Sprintf( - "[::b]%s[-]\n\n[::b]Basic Settings:[-]\n Host: [white]%s[-]\n User: [white]%s[-]\n Port: [white]%s[-]\n Key: [white]%s[-]\n Tags: %s\n Pinned: [white]%s[-]\n Last SSH: %s\n SSH Count: [white]%d[-]\n", + "[::b]%s[-]\n\n[::b]Basic Settings:[-]\n Host: [white]%s[-]\n User: [white]%s[-]\n Port: [white]%s[-]\n Key: [white]%s[-]\n Tags: %s\n Pinned: [white]%s[-]\n Hidden: [white]%s[-]\n Last SSH: %s\n SSH Count: [white]%d[-]\n", aliasText, hostText, userText, portText, - serverKey, tagsText, pinnedStr, + serverKey, tagsText, pinnedStr, hiddenStr, lastSeen, server.SSHCount) // Advanced settings section (only show non-empty fields) @@ -213,7 +217,7 @@ func (sd *ServerDetails) UpdateServer(server domain.Server) { } // Commands list - text += "\n[::b]Commands:[-]\n Enter: SSH connect\n c: Copy SSH command\n g: Ping server\n r: Refresh list\n a: Add new server\n e: Edit entry\n t: Edit tags\n d: Delete entry\n p: Pin/Unpin" + text += "\n[::b]Commands:[-]\n Enter: SSH connect\n c: Copy SSH command\n g: Ping server\n r: Refresh list\n a: Add new server\n e: Edit entry\n t: Edit tags\n d: Delete entry\n p: Pin/Unpin\n h: Hide/Unhide\n H: Toggle hidden view" sd.TextView.SetText(text) } diff --git a/internal/adapters/ui/status_bar.go b/internal/adapters/ui/status_bar.go index d8ca0aa..150c8fd 100644 --- a/internal/adapters/ui/status_bar.go +++ b/internal/adapters/ui/status_bar.go @@ -20,7 +20,7 @@ import ( ) func DefaultStatusText() string { - return "[white]↑↓[-] Navigate β€’ [white]Enter[-] SSH β€’ [white]c[-] Copy SSH β€’ [white]a[-] Add β€’ [white]e[-] Edit β€’ [white]g[-] Ping β€’ [white]d[-] Delete β€’ [white]p[-] Pin/Unpin β€’ [white]/[-] Search β€’ [white]q[-] Quit" + return "[white]↑↓[-] Navigate β€’ [white]Enter[-] SSH β€’ [white]c[-] Copy SSH β€’ [white]a[-] Add β€’ [white]e[-] Edit β€’ [white]g[-] Ping β€’ [white]d[-] Delete β€’ [white]p[-] Pin/Unpin β€’ [white]h[-] Hide β€’ [white]H[-] Hidden view β€’ [white]/[-] Search β€’ [white]q[-] Quit" } func NewStatusBar() *tview.TextView { diff --git a/internal/adapters/ui/tui.go b/internal/adapters/ui/tui.go index 6046071..7137455 100644 --- a/internal/adapters/ui/tui.go +++ b/internal/adapters/ui/tui.go @@ -18,6 +18,7 @@ import ( "github.com/gdamore/tcell/v2" "go.uber.org/zap" + "github.com/Adembc/lazyssh/internal/core/domain" "github.com/Adembc/lazyssh/internal/core/ports" "github.com/rivo/tview" ) @@ -48,6 +49,7 @@ type tui struct { sortMode SortMode searchVisible bool + showHidden bool } func NewTUI(logger *zap.SugaredLogger, ss ports.ServerService, version, commit string) App { @@ -102,6 +104,7 @@ func (t *tui) buildComponents() *tui { // default sort mode t.sortMode = SortByAliasAsc + t.showHidden = false return t } @@ -131,8 +134,11 @@ func (t *tui) bindEvents() *tui { } func (t *tui) loadInitialData() *tui { - servers, _ := t.serverService.ListServers("") - sortServersForUI(servers, t.sortMode) + servers, _, err := t.fetchServers("", t.showHidden, t.sortMode) + if err != nil { + t.logger.Errorw("failed to load servers", "error", err) + servers = nil + } t.updateListTitle() t.serverList.UpdateServers(servers) @@ -141,6 +147,20 @@ func (t *tui) loadInitialData() *tui { func (t *tui) updateListTitle() { if t.serverList != nil { - t.serverList.SetTitle(" Servers β€” Sort: " + t.sortMode.String() + " ") + hiddenText := "hidden off" + if t.showHidden { + hiddenText = "hidden on" + } + t.serverList.SetTitle(" Servers β€” Sort: " + t.sortMode.String() + " β€” " + hiddenText + " ") + } +} + +func (t *tui) fetchServers(query string, includeHidden bool, sortMode SortMode) ([]domain.Server, int, error) { + servers, err := t.serverService.ListServers(query) + if err != nil { + return nil, 0, err } + filtered, hiddenCount := filterServersByHidden(servers, includeHidden) + sortServersForUI(filtered, sortMode) + return filtered, hiddenCount, nil } diff --git a/internal/adapters/ui/utils.go b/internal/adapters/ui/utils.go index 95a3996..141c848 100644 --- a/internal/adapters/ui/utils.go +++ b/internal/adapters/ui/utils.go @@ -80,8 +80,23 @@ func pinnedIcon(pinnedAt time.Time) string { func formatServerLine(s domain.Server) (primary, secondary string) { icon := cellPad(pinnedIcon(s.PinnedAt), 2) - // Use a consistent color for alias; the icon reflects pinning - primary = fmt.Sprintf("%s [white::b]%-12s[-] [#AAAAAA]%-18s[-] [#888888]Last SSH: %s[-] %s", icon, s.Alias, s.Host, humanizeDuration(s.LastSeen), renderTagBadgesForList(s.Tags)) + + aliasStyle := "[white::b]" + hostStyle := "[#AAAAAA]" + statusParts := make([]string, 0, 1) + if s.Hidden { + aliasStyle = "[#909090::b]" + hostStyle = "[#777777]" + statusParts = append(statusParts, "πŸ™ˆ [#FFB347]hidden[-]") + } + + status := "" + if len(statusParts) > 0 { + status = " " + strings.Join(statusParts, " ") + } + + primary = fmt.Sprintf("%s %s%-12s[-] %s%-18s[-] [#888888]Last SSH: %s[-]%s %s", + icon, aliasStyle, s.Alias, hostStyle, s.Host, humanizeDuration(s.LastSeen), status, renderTagBadgesForList(s.Tags)) secondary = "" return } @@ -120,6 +135,21 @@ func humanizeDuration(t time.Time) string { return fmt.Sprintf("%dy ago", years) } +func filterServersByHidden(servers []domain.Server, includeHidden bool) ([]domain.Server, int) { + filtered := make([]domain.Server, 0, len(servers)) + hiddenCount := 0 + for _, srv := range servers { + if srv.Hidden { + hiddenCount++ + if !includeHidden { + continue + } + } + filtered = append(filtered, srv) + } + return filtered, hiddenCount +} + // BuildSSHCommand constructs a ready-to-run ssh command for the given server. // Format: ssh [options] [user@]host [command] func BuildSSHCommand(s domain.Server) string { diff --git a/internal/adapters/ui/utils_test.go b/internal/adapters/ui/utils_test.go index aad5ad8..d6b0e3a 100644 --- a/internal/adapters/ui/utils_test.go +++ b/internal/adapters/ui/utils_test.go @@ -171,3 +171,33 @@ func TestBuildSSHCommand_CompleteCommand(t *testing.T) { t.Errorf("Command should contain 'admin@example.com', got: %q", result) } } + +func TestFilterServersByHidden(t *testing.T) { + servers := []domain.Server{ + {Alias: "a", Hidden: false}, + {Alias: "b", Hidden: true}, + {Alias: "c", Hidden: true}, + {Alias: "d", Hidden: false}, + } + + filtered, hiddenCount := filterServersByHidden(servers, false) + if hiddenCount != 2 { + t.Fatalf("expected hiddenCount=2, got %d", hiddenCount) + } + if len(filtered) != 2 { + t.Fatalf("expected 2 visible servers, got %d", len(filtered)) + } + for _, srv := range filtered { + if srv.Hidden { + t.Fatalf("expected filtered servers to exclude hidden ones, got %+v", srv) + } + } + + filtered, hiddenCount = filterServersByHidden(servers, true) + if hiddenCount != 2 { + t.Fatalf("expected hiddenCount to remain 2 when including hidden, got %d", hiddenCount) + } + if len(filtered) != len(servers) { + t.Fatalf("expected all servers when including hidden, got %d", len(filtered)) + } +} diff --git a/internal/core/domain/server.go b/internal/core/domain/server.go index c23b301..019e354 100644 --- a/internal/core/domain/server.go +++ b/internal/core/domain/server.go @@ -27,6 +27,7 @@ type Server struct { LastSeen time.Time PinnedAt time.Time SSHCount int + Hidden bool // Additional SSH config fields // Connection and proxy settings diff --git a/internal/core/ports/repositories.go b/internal/core/ports/repositories.go index 133e4f8..ebff98f 100644 --- a/internal/core/ports/repositories.go +++ b/internal/core/ports/repositories.go @@ -22,5 +22,6 @@ type ServerRepository interface { AddServer(server domain.Server) error DeleteServer(server domain.Server) error SetPinned(alias string, pinned bool) error + SetHidden(alias string, hidden bool) error RecordSSH(alias string) error } diff --git a/internal/core/ports/services.go b/internal/core/ports/services.go index 9394751..7862ee8 100644 --- a/internal/core/ports/services.go +++ b/internal/core/ports/services.go @@ -26,6 +26,7 @@ type ServerService interface { AddServer(server domain.Server) error DeleteServer(server domain.Server) error SetPinned(alias string, pinned bool) error + SetHidden(alias string, hidden bool) error SSH(alias string) error Ping(server domain.Server) (bool, time.Duration, error) } diff --git a/internal/core/services/server_service.go b/internal/core/services/server_service.go index c01b18b..963d466 100644 --- a/internal/core/services/server_service.go +++ b/internal/core/services/server_service.go @@ -148,6 +148,15 @@ func (s *serverService) SetPinned(alias string, pinned bool) error { return err } +// SetHidden sets or clears the hidden state for the given server alias. +func (s *serverService) SetHidden(alias string, hidden bool) error { + err := s.serverRepository.SetHidden(alias, hidden) + if err != nil { + s.logger.Errorw("failed to set hidden state", "error", err, "alias", alias, "hidden", hidden) + } + return err +} + // SSH starts an interactive SSH session to the given alias using the system's ssh client. func (s *serverService) SSH(alias string) error { s.logger.Infow("ssh start", "alias", alias)