Skip to content

Commit eb5ce70

Browse files
author
ant
committed
feat(ui): add config export/import and fix i18n for tabs/help/modals
- Add ExportServers/ImportServers to repository and service - Fix path expansion for ~/ in export/import (use HasPrefix instead of IsAbs) - Add keyboard shortcuts E/I for export/import - Add i18n translations for export/import functionality - Fix Tab labels to use i18n translations - Fix Help panel title and content formatting to use i18n - Fix modal buttons and validation error messages to use i18n - Add language suffix to build output filenames (lazyssh-zh-CN) - Add helper functions for tab name and abbrev translations
1 parent 16ea54f commit eb5ce70

9 files changed

Lines changed: 400 additions & 66 deletions

File tree

build.sh

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -123,36 +123,39 @@ build_binary() {
123123
fi
124124
}
125125

126+
LANG_SUFFIX=""
127+
if [[ "$LANG" != "en" ]]; then
128+
LANG_SUFFIX="-${LANG}"
129+
fi
130+
126131
log_info "LazySSH Build"
127132
log_info "Version: ${VERSION}"
128133
log_info "Commit: ${COMMIT}"
129134
log_info "Language: ${LANG}"
130135
log_info "Output: ${OUTPUT_DIR}"
131136
echo ""
132137

133-
# Clean previous build
134138
rm -f "${OUTPUT_DIR}"/lazyssh* 2>/dev/null || true
135139

136140
if [[ "$PARALLEL" == true ]]; then
137141
log_info "Building for all platforms..."
138142
echo ""
139143

140-
build_binary "linux" "amd64" "lazyssh-linux-amd64"
141-
build_binary "linux" "arm64" "lazyssh-linux-arm64"
142-
build_binary "darwin" "amd64" "lazyssh-darwin-amd64"
143-
build_binary "darwin" "arm64" "lazyssh-darwin-arm64"
144-
build_binary "windows" "amd64" "lazyssh-windows-amd64.exe"
144+
build_binary "linux" "amd64" "lazyssh${LANG_SUFFIX}-linux-amd64"
145+
build_binary "linux" "arm64" "lazyssh${LANG_SUFFIX}-linux-arm64"
146+
build_binary "darwin" "amd64" "lazyssh${LANG_SUFFIX}-darwin-amd64"
147+
build_binary "darwin" "arm64" "lazyssh${LANG_SUFFIX}-darwin-arm64"
148+
build_binary "windows" "amd64" "lazyssh${LANG_SUFFIX}-windows-amd64.exe"
145149

146150
echo ""
147151
log_success "All builds complete!"
148152
else
149-
# Build for current platform
150153
CURRENT_OS=$(go env GOOS)
151154
CURRENT_ARCH=$(go env GOARCH)
152155

153-
build_binary "$CURRENT_OS" "$CURRENT_ARCH" "lazyssh"
156+
build_binary "$CURRENT_OS" "$CURRENT_ARCH" "lazyssh${LANG_SUFFIX}"
154157

155158
echo ""
156-
log_success "Build complete: ${OUTPUT_DIR}/lazyssh"
157-
log_info "Run: ${OUTPUT_DIR}/lazyssh"
159+
log_success "Build complete: ${OUTPUT_DIR}/lazyssh${LANG_SUFFIX}"
160+
log_info "Run: ${OUTPUT_DIR}/lazyssh${LANG_SUFFIX}"
158161
fi

internal/adapters/data/ssh_config_file/ssh_config_file_repo.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
package ssh_config_file
1616

1717
import (
18+
"encoding/json"
1819
"fmt"
20+
"os"
21+
"path/filepath"
22+
"strings"
23+
"time"
1924

2025
"github.com/Adembc/lazyssh/internal/core/crypto"
2126
"github.com/Adembc/lazyssh/internal/core/domain"
@@ -192,3 +197,129 @@ func (r *Repository) SetPassword(alias string, password string) error {
192197
func (r *Repository) GetPassword(alias string) (string, error) {
193198
return r.metadataManager.getPassword(alias)
194199
}
200+
201+
func (r *Repository) ExportServers(path string) error {
202+
servers, err := r.ListServers("")
203+
if err != nil {
204+
return fmt.Errorf("failed to list servers: %w", err)
205+
}
206+
207+
exportMeta := make(map[string]ports.ServerExportMeta)
208+
for _, server := range servers {
209+
meta := ports.ServerExportMeta{
210+
Tags: server.Tags,
211+
SSHCount: server.SSHCount,
212+
}
213+
if !server.PinnedAt.IsZero() {
214+
meta.PinnedAt = server.PinnedAt.Format(time.RFC3339)
215+
}
216+
if !server.LastSeen.IsZero() {
217+
meta.LastSeen = server.LastSeen.Format(time.RFC3339)
218+
}
219+
if server.Password != "" {
220+
meta.Password = server.Password
221+
}
222+
exportMeta[server.Alias] = meta
223+
}
224+
225+
data := ports.ExportData{
226+
Version: "1.0",
227+
Servers: servers,
228+
Metadata: exportMeta,
229+
ExportedAt: time.Now().Format(time.RFC3339),
230+
}
231+
232+
absPath := path
233+
if strings.HasPrefix(path, "~") {
234+
home, _ := os.UserHomeDir()
235+
relPath := strings.TrimPrefix(path, "~")
236+
relPath = strings.TrimPrefix(relPath, "/")
237+
absPath = filepath.Join(home, relPath)
238+
} else if !filepath.IsAbs(path) {
239+
home, _ := os.UserHomeDir()
240+
absPath = filepath.Join(home, path)
241+
}
242+
243+
jsonData, err := json.MarshalIndent(data, "", " ")
244+
if err != nil {
245+
return fmt.Errorf("failed to marshal export data: %w", err)
246+
}
247+
248+
if err := os.WriteFile(absPath, jsonData, 0644); err != nil {
249+
return fmt.Errorf("failed to write export file: %w", err)
250+
}
251+
252+
return nil
253+
}
254+
255+
func (r *Repository) ImportServers(path string, merge bool) (int, int, error) {
256+
absPath := path
257+
if strings.HasPrefix(path, "~") {
258+
home, _ := os.UserHomeDir()
259+
relPath := strings.TrimPrefix(path, "~")
260+
relPath = strings.TrimPrefix(relPath, "/")
261+
absPath = filepath.Join(home, relPath)
262+
} else if !filepath.IsAbs(path) {
263+
home, _ := os.UserHomeDir()
264+
absPath = filepath.Join(home, path)
265+
}
266+
267+
jsonData, err := os.ReadFile(absPath)
268+
if err != nil {
269+
return 0, 0, fmt.Errorf("failed to read import file: %w", err)
270+
}
271+
272+
var data ports.ExportData
273+
if err := json.Unmarshal(jsonData, &data); err != nil {
274+
return 0, 0, fmt.Errorf("failed to parse import file: %w", err)
275+
}
276+
277+
cfg, err := r.loadConfig()
278+
if err != nil {
279+
return 0, 0, fmt.Errorf("failed to load config: %w", err)
280+
}
281+
282+
imported := 0
283+
skipped := 0
284+
285+
for _, server := range data.Servers {
286+
exists := r.serverExists(cfg, server.Alias)
287+
288+
if exists && !merge {
289+
host := r.findHostByAlias(cfg, server.Alias)
290+
if host != nil {
291+
r.updateHostNodes(host, server)
292+
imported++
293+
}
294+
} else if !exists {
295+
host := r.createHostFromServer(server)
296+
cfg.Hosts = append(cfg.Hosts, host)
297+
imported++
298+
} else {
299+
skipped++
300+
}
301+
302+
if meta, ok := data.Metadata[server.Alias]; ok {
303+
password := meta.Password
304+
server.Password = password
305+
server.Tags = meta.Tags
306+
if meta.PinnedAt != "" {
307+
server.PinnedAt, _ = time.Parse(time.RFC3339, meta.PinnedAt)
308+
}
309+
if meta.LastSeen != "" {
310+
server.LastSeen, _ = time.Parse(time.RFC3339, meta.LastSeen)
311+
}
312+
server.SSHCount = meta.SSHCount
313+
server.Password = ""
314+
_ = r.metadataManager.updateServer(server, server.Alias, password)
315+
}
316+
}
317+
318+
if imported > 0 {
319+
if err := r.saveConfig(cfg); err != nil {
320+
return imported, skipped, fmt.Errorf("failed to save config: %w", err)
321+
}
322+
}
323+
324+
return imported, skipped, nil
325+
}

internal/adapters/ui/handlers.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ func (t *tui) handleGlobalKeys(event *tcell.EventKey) *tcell.EventKey {
9393
case 'k':
9494
t.handleNavigateUp()
9595
return nil
96+
case 'E':
97+
t.handleExport()
98+
return nil
99+
case 'I':
100+
t.handleImport()
101+
return nil
96102
}
97103

98104
if event.Key() == tcell.KeyEnter {
@@ -645,3 +651,96 @@ func (t *tui) handleStopForwarding() {
645651
}()
646652
}
647653
}
654+
655+
func (t *tui) handleExport() {
656+
form := tview.NewForm()
657+
form.SetTitle(i18n.T("export.title"))
658+
form.SetBorder(true)
659+
660+
pathField := tview.NewInputField()
661+
pathField.SetLabel(i18n.T("export.path_label"))
662+
pathField.SetPlaceholder("~/lazyssh-export.json")
663+
pathField.SetText("~/lazyssh-export.json")
664+
665+
form.AddFormItem(pathField)
666+
form.AddButton(i18n.T("export.export_btn"), func() {
667+
path := pathField.GetText()
668+
go func() {
669+
err := t.serverService.ExportServers(path)
670+
t.app.QueueUpdateDraw(func() {
671+
if err != nil {
672+
modal := tview.NewModal().
673+
SetText(fmt.Sprintf(i18n.T("export.failed"), err.Error())).
674+
AddButtons([]string{i18n.T("common.close")}).
675+
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
676+
t.returnToMain()
677+
})
678+
t.app.SetRoot(modal, true)
679+
} else {
680+
modal := tview.NewModal().
681+
SetText(fmt.Sprintf(i18n.T("export.success_msg"), path)).
682+
AddButtons([]string{i18n.T("common.close")}).
683+
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
684+
t.returnToMain()
685+
})
686+
t.app.SetRoot(modal, true)
687+
}
688+
})
689+
}()
690+
})
691+
form.AddButton(i18n.T("common.cancel"), func() {
692+
t.returnToMain()
693+
})
694+
695+
t.app.SetRoot(form, true)
696+
}
697+
698+
func (t *tui) handleImport() {
699+
form := tview.NewForm()
700+
form.SetTitle(i18n.T("import.title"))
701+
form.SetBorder(true)
702+
703+
pathField := tview.NewInputField()
704+
pathField.SetLabel(i18n.T("import.path_label"))
705+
pathField.SetPlaceholder("~/lazyssh-export.json")
706+
707+
mergeField := tview.NewCheckbox()
708+
mergeField.SetLabel(i18n.T("import.merge_label"))
709+
mergeField.SetChecked(true)
710+
711+
form.AddFormItem(pathField)
712+
form.AddFormItem(mergeField)
713+
form.AddButton(i18n.T("import.import_btn"), func() {
714+
path := pathField.GetText()
715+
merge := mergeField.IsChecked()
716+
go func() {
717+
imported, skipped, err := t.serverService.ImportServers(path, merge)
718+
t.app.QueueUpdateDraw(func() {
719+
if err != nil {
720+
modal := tview.NewModal().
721+
SetText(fmt.Sprintf(i18n.T("import.failed"), err.Error())).
722+
AddButtons([]string{i18n.T("common.close")}).
723+
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
724+
t.returnToMain()
725+
})
726+
t.app.SetRoot(modal, true)
727+
} else {
728+
msg := fmt.Sprintf(i18n.T("import.success_msg"), imported, skipped)
729+
modal := tview.NewModal().
730+
SetText(msg).
731+
AddButtons([]string{i18n.T("common.close")}).
732+
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
733+
t.returnToMain()
734+
t.refreshServerList()
735+
})
736+
t.app.SetRoot(modal, true)
737+
}
738+
})
739+
}()
740+
})
741+
form.AddButton(i18n.T("common.cancel"), func() {
742+
t.returnToMain()
743+
})
744+
745+
t.app.SetRoot(form, true)
746+
}

0 commit comments

Comments
 (0)