diff --git a/cmd/ggh.go b/cmd/ggh.go index 31d06d0..5c89c52 100644 --- a/cmd/ggh.go +++ b/cmd/ggh.go @@ -17,7 +17,7 @@ func Main() { action, value := command.Which() switch action { case command.InteractiveHistory: - args = history.Interactive() + args = interactive.History() case command.InteractiveConfig: args = interactive.Config("") case command.InteractiveConfigWithSearch: diff --git a/internal/config/file.go b/internal/config/file.go index b8e1b0f..b69e9cb 100644 --- a/internal/config/file.go +++ b/internal/config/file.go @@ -5,14 +5,19 @@ import ( "path/filepath" ) -func GetSshDir() string { +func HomeDir() string { userHomeDir, err := os.UserHomeDir() if err != nil { return "" } - return filepath.Join(userHomeDir, ".ssh") + return userHomeDir +} + +func GetSshDir() string { + + return filepath.Join(HomeDir(), ".ssh") } func GetConfigFile() string { diff --git a/internal/config/parser.go b/internal/config/parser.go index 0c6adef..1270e55 100644 --- a/internal/config/parser.go +++ b/internal/config/parser.go @@ -35,10 +35,6 @@ func ParseWithSearch(search string, configFile string) ([]SSHConfig, error) { continue } - if search != "" && !strings.Contains(lines[0], search) { - continue - } - sshConfig := SSHConfig{ Name: lines[0], Port: "", @@ -58,7 +54,7 @@ func ParseWithSearch(search string, configFile string) ([]SSHConfig, error) { } switch { case strings.Contains(line, "Include"): - result, err := ParseInclude(value) + result, err := ParseInclude(search, value) if err != nil { panic(err) } @@ -74,36 +70,61 @@ func ParseWithSearch(search string, configFile string) ([]SSHConfig, error) { } } - if sshConfig.Host != "" { - configs = append(configs, sshConfig) + if sshConfig.Host == "" || !strings.Contains(sshConfig.Name, search) { + continue } + + configs = append(configs, sshConfig) + } return configs, nil } -func ParseInclude(path string) ([]SSHConfig, error) { +func ParseInclude(search string, path string) ([]SSHConfig, error) { var results = make([]SSHConfig, 0) - if filepath.IsLocal(path) { + var isAbsolute = path[0] == '/' || path[0] == '~' + + var paths []string + var err error + + if isAbsolute { + if path[0] == '~' { + path = filepath.Join(HomeDir(), path[2:]) + } + } else { path = filepath.Join(GetSshDir(), path) } - info, err := os.Stat(path) - if err != nil || info.IsDir() { - return results, err - } + paths, err = filepath.Glob(path) - fileContent, err := os.ReadFile(path) if err != nil { return nil, err } - items, err := Parse(string(fileContent)) - if err != nil { - return nil, err + for _, path := range paths { + info, err := os.Stat(path) + + if err != nil { + return nil, err + } + + if info.IsDir() { + continue + } + + fileContent, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + items, err := ParseWithSearch(search, string(fileContent)) + if err != nil { + return nil, err + } + results = append(results, items...) } - results = append(results, items...) return results, nil } diff --git a/internal/history/fetch.go b/internal/history/fetch.go index c7f467d..44269f5 100644 --- a/internal/history/fetch.go +++ b/internal/history/fetch.go @@ -4,12 +4,9 @@ import ( "encoding/json" "fmt" "github.com/byawitz/ggh/internal/config" - "github.com/byawitz/ggh/internal/interactive" - "github.com/byawitz/ggh/internal/ssh" "github.com/byawitz/ggh/internal/theme" "github.com/charmbracelet/bubbles/table" "log" - "os" "time" ) @@ -34,33 +31,21 @@ func Fetch(file []byte) ([]SSHHistory, error) { return nil, err } - return historyList, nil -} -func Interactive() []string { - list, err := FetchWithDefaultFile() + search, err := config.ParseWithSearch("", config.GetConfigFile()) if err != nil { - log.Fatal(err) + return historyList, nil } - if len(list) == 0 { - fmt.Println("No history found.") - os.Exit(0) + for i, history := range historyList { + for _, sshConfig := range search { + if sshConfig.Host == history.Connection.Host { + historyList[i].Connection.Name = sshConfig.Name + } + } } - var rows []table.Row - currentTime := time.Now() - for _, history := range list { - rows = append(rows, table.Row{ - history.Connection.Host, - history.Connection.Port, - history.Connection.User, - history.Connection.Key, - fmt.Sprintf("%s", readableTime(currentTime.Sub(history.Date))), - }) - } - c := interactive.Select(rows, interactive.SelectHistory) - return ssh.GenerateCommandArgs(c) + return historyList, nil } func Print() { @@ -82,14 +67,14 @@ func Print() { history.Connection.Port, history.Connection.User, history.Connection.Key, - fmt.Sprintf("%s", readableTime(currentTime.Sub(history.Date))), + fmt.Sprintf("%s", ReadableTime(currentTime.Sub(history.Date))), }) } fmt.Println(theme.PrintTable(rows, theme.PrintHistory)) } -func readableTime(d time.Duration) string { +func ReadableTime(d time.Duration) string { if d.Seconds() < 60 { return fmt.Sprintf("%d seconds ago", int(d.Seconds())) } diff --git a/internal/history/save.go b/internal/history/save.go index d4b3cfd..f0c19b9 100644 --- a/internal/history/save.go +++ b/internal/history/save.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "github.com/byawitz/ggh/internal/config" + "github.com/charmbracelet/bubbles/table" "os" "slices" "strings" @@ -14,7 +15,6 @@ func AddHistoryFromArgs(args []string) { if len(args) == 1 && !strings.Contains(args[0], "@") { localConfig, err := config.GetConfig(args[0]) if err != nil || localConfig.Name == "" { - fmt.Printf("couldn't fetch %s from config file, error:%v.\n", args[0], err) return } @@ -70,6 +70,33 @@ func AddHistory(c config.SSHConfig) { } } +func RemoveByIP(row table.Row) { + list, err := Fetch(getFile()) + + if err != nil { + fmt.Println("error getting ggh file") + return + } + + ip := row[1] + + saving := make([]SSHHistory, 0, len(list)-1) + + for _, item := range list { + if item.Connection.Host == ip { + continue + } + + saving = append(saving, item) + } + + err = saveFile(SSHHistory{}, saving) + if err != nil { + panic("error saving ggh file") + } + +} + func saveFile(n SSHHistory, l []SSHHistory) error { file := getFileLocation() fileContent := stringify(n, l) @@ -89,7 +116,10 @@ func stringify(n SSHHistory, l []SSHHistory) string { } } - history = append(history, n) + if n.Connection.Host != "" { + history = append(history, n) + } + history = append(history, l...) content, err := json.Marshal(history) diff --git a/internal/interactive/display.go b/internal/interactive/display.go index ba3042b..e62899f 100644 --- a/internal/interactive/display.go +++ b/internal/interactive/display.go @@ -3,9 +3,12 @@ package interactive import ( "fmt" "github.com/byawitz/ggh/internal/config" + "github.com/byawitz/ggh/internal/history" "github.com/byawitz/ggh/internal/ssh" "github.com/charmbracelet/bubbles/table" + "log" "os" + "time" ) func Config(value string) []string { @@ -28,3 +31,31 @@ func Config(value string) []string { c := Select(rows, SelectConfig) return ssh.GenerateCommandArgs(c) } + +func History() []string { + list, err := history.FetchWithDefaultFile() + + if err != nil { + log.Fatal(err) + } + + if len(list) == 0 { + fmt.Println("No history found.") + os.Exit(0) + } + + var rows []table.Row + currentTime := time.Now() + for _, historyItem := range list { + rows = append(rows, table.Row{ + historyItem.Connection.Name, + historyItem.Connection.Host, + historyItem.Connection.Port, + historyItem.Connection.User, + historyItem.Connection.Key, + fmt.Sprintf("%s", history.ReadableTime(currentTime.Sub(historyItem.Date))), + }) + } + c := Select(rows, SelectHistory) + return ssh.GenerateCommandArgs(c) +} diff --git a/internal/interactive/select.go b/internal/interactive/select.go index efeef19..2d95b70 100644 --- a/internal/interactive/select.go +++ b/internal/interactive/select.go @@ -3,9 +3,12 @@ package interactive import ( "fmt" "github.com/byawitz/ggh/internal/config" + "github.com/byawitz/ggh/internal/history" "github.com/byawitz/ggh/internal/theme" "math" "os" + "slices" + "strings" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" @@ -33,6 +36,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { + case "d": + history.RemoveByIP(m.table.SelectedRow()) + + rows := slices.Delete(m.table.Rows(), m.table.Cursor(), m.table.Cursor()+1) + m.table.SetRows(rows) + + m.table, cmd = m.table.Update("") // Overrides default `d` behavior + return m, cmd case "q", "ctrl+c", "esc": m.exit = true return m, tea.Quit @@ -46,20 +57,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func setConfig(row table.Row, what Selecting) config.SSHConfig { - if what == SelectConfig { - return config.SSHConfig{ - Host: row[1], - Port: row[2], - User: row[3], - Key: row[4], - } - } - return config.SSHConfig{ - Host: row[0], - Port: row[1], - User: row[2], - Key: row[3], + Host: row[1], + Port: row[2], + User: row[3], + Key: row[4], } } @@ -67,7 +69,7 @@ func (m model) View() string { if m.choice.Host != "" || m.exit { return "" } - return theme.BaseStyle.Render(m.table.View()) + "\n " + m.table.HelpView() + "\n" + return theme.BaseStyle.Render(m.table.View()) + "\n " + m.HelpView() + "\n" } func Select(rows []table.Row, what Selecting) config.SSHConfig { @@ -84,8 +86,9 @@ func Select(rows []table.Row, what Selecting) config.SSHConfig { if what == SelectHistory { columns = append(columns, []table.Column{ + {Title: "Name", Width: 10}, {Title: "Host", Width: 15}, - {Title: "Port", Width: 10}, + {Title: "Port", Width: 4}, {Title: "User", Width: 10}, {Title: "Key", Width: 10}, {Title: "Last login", Width: 15}, @@ -123,3 +126,49 @@ func Select(rows []table.Row, what Selecting) config.SSHConfig { return config.SSHConfig{} } +func (m model) HelpView() string { + + km := table.DefaultKeyMap() + + var b strings.Builder + + b.WriteString(generateHelpBlock(km.LineUp.Help().Key, km.LineUp.Help().Desc, true)) + b.WriteString(generateHelpBlock(km.LineDown.Help().Key, km.LineDown.Help().Desc, true)) + + if m.what == SelectHistory { + b.WriteString(generateHelpBlock("d", "delete", true)) + } + + b.WriteString(generateHelpBlock("q/esc", "quit", false)) + + return b.String() +} + +func generateHelpBlock(key, desc string, withSep bool) string { + keyStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ + Light: "#909090", + Dark: "#626262", + }) + + descStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ + Light: "#B2B2B2", + Dark: "#4A4A4A", + }) + + sepStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ + Light: "#DDDADA", + Dark: "#3C3C3C", + }) + + sep := sepStyle.Inline(true).Render(" • ") + + str := keyStyle.Inline(true).Render(key) + + " " + + descStyle.Inline(true).Render(desc) + + if withSep { + str += sep + } + + return str +}