package main import ( "log/slog" "os" "path/filepath" "strings" "time" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) func init() { logger := slog.New(&Handler{os.Stderr, slog.LevelInfo}) slog.SetDefault(logger) } func main() { renderUI() } func renderUI() { // SQL-files list files, _ := filepath.Glob(filepath.Join(SQL_FILES_DIR, "*.sql")) for i := range files { files[i] = filepath.Base(files[i]) } // Servers file list configs, _ := filepath.Glob("*.ini") configFileDropDown := tview.NewDropDown(). SetOptions(configs, nil). SetLabel("Файл настроек серверов"). SetCurrentOption(0) sqlFileDropDown := tview.NewDropDown(). SetOptions(files, nil). SetLabel("Файл SQL-скрипта"). SetCurrentOption(0) encodingDropDown := tview.NewDropDown(). SetOptions([]string{string(EncodingWin1251), string(EncodingUtf8)}, nil). SetLabel("Кодировка CSV-файла") encodingDropDown.SetDisabled(true) encodingDropDown.SetCurrentOption(0) formatDropDown := tview.NewDropDown(). SetOptions([]string{string(ExportFormatExcel), string(ExportFormatCsv)}, func(text string, index int) { if text == string(ExportFormatCsv) { encodingDropDown.SetDisabled(false) return } encodingDropDown.SetDisabled(true) }). SetLabel("Формат выгрузки").SetCurrentOption(0) form := tview.NewForm(). AddFormItem(configFileDropDown). AddFormItem(sqlFileDropDown). AddFormItem(formatDropDown). AddFormItem(encodingDropDown) form.SetTitle("Параметры").SetBorder(true) app := tview.NewApplication() grid := tview.NewGrid().SetRows(-1, 1). AddItem(form, 0, 0, 1, 1, 0, 0, true). AddItem(tview.NewButton("Запуск").SetSelectedFunc(func() { _, scriptFilePath := sqlFileDropDown.GetCurrentOption() _, exportFileFormatStr := formatDropDown.GetCurrentOption() _, configFilePath := configFileDropDown.GetCurrentOption() _, encodingStr := encodingDropDown.GetCurrentOption() job := &Job{ configFilePath: configFilePath, scriptFilePath: filepath.Join(SQL_FILES_DIR, scriptFilePath), exportFileFormat: ExportFormat(exportFileFormatStr), encoding: Encoding(encodingStr)} err := job.init() if err != nil { showError(app, err) return } renderProgressUI(app, job) }), 1, 0, 1, 1, 0, 0, false) grid.SetBorderPadding(0, 1, 1, 1) form.SetTitle("Параметры выгрузки").SetTitleAlign(tview.AlignLeft) if err := app.SetRoot(grid, true).EnableMouse(true).Run(); err != nil { panic(err) } } func renderProgressUI(app *tview.Application, job *Job) { statusTextViews := make([]*tview.TextView, 0) maxServerNameLen := 0 for i := range job.servers { if len(job.servers[i].Name) > maxServerNameLen { maxServerNameLen = len([]rune(job.servers[i].Name)) + 2 } } rows := make([]int, len(job.servers)+3) for i := range rows { rows[i] = 1 } grid := tview.NewGrid().SetRows(rows...).SetColumns(1, maxServerNameLen+2) for i, server := range job.servers { grid.AddItem(tview.NewTextView().SetTextAlign(tview.AlignLeft).SetText(server.Name), i+1, 1, 1, 1, 0, 0, false) p := tview.NewTextView().SetTextAlign(tview.AlignLeft).SetText("...") statusTextViews = append(statusTextViews, p) grid.AddItem(p, i+1, 2, 1, 1, 0, 0, false) } grid.AddItem(tview.NewTextView().SetTextAlign(tview.AlignLeft).SetText(strings.Repeat("-", maxServerNameLen)), len(job.servers)+1, 1, 1, 1, 0, 0, false) grid.AddItem(tview.NewTextView().SetTextAlign(tview.AlignLeft), len(job.servers)+1, 2, 1, 1, 0, 0, false) statusTextView := tview.NewTextView() grid.AddItem(statusTextView, len(job.servers)+2, 2, 1, 1, 0, 0, false) // Last empty line grid.AddItem(tview.NewTextView(), len(job.servers)+3, 1, 1, 1, 0, 0, false) grid.AddItem(tview.NewTextView(), len(job.servers)+3, 2, 1, 1, 0, 0, false) go func() { oneMoreUpdate := false for !job.isFinished || oneMoreUpdate { if !oneMoreUpdate { time.Sleep(time.Second) } for i, server := range job.servers { s := []string{server.Status} if server.Error != nil { s = append(s, server.Error.Error()) } statusTextViews[i].SetText(strings.Join(s, ": ")) if server.Error != nil { statusTextViews[i].SetTextColor(tcell.ColorRed) } else if strings.HasPrefix(server.Status, "ЗАВЕРШЕНО: ") { statusTextViews[i].SetTextColor(tcell.ColorGreen) } } statusTextView.SetText(job.status) if strings.HasPrefix(job.status, "ЗАВЕРШЕНО") { statusTextView.SetTextColor(tcell.ColorGreen) } app.ForceDraw() if job.isFinished { if !oneMoreUpdate { oneMoreUpdate = true } else { break } } } // TODO: /*finishButton := tview.NewButton("OK").SetSelectedFunc(func() { app.Stop() }).SetBackgroundColor(tcell.ColorDarkGreen) grid.AddItem(finishButton, len(job.servers)+2, 1, 1, 1, 0, 0, false) app.ForceDraw().SetFocus(finishButton)*/ showMessage(app, "Выгрузка завершена.") }() go func() { err := job.launch() if err != nil { showError(app, err) } }() if err := app.SetRoot(grid, true).SetFocus(grid).Run(); err != nil { panic(err) } } func showError(app *tview.Application, err error) { modal := tview.NewModal(). SetText(err.Error()). AddButtons([]string{"OK"}). SetBackgroundColor(tcell.ColorRed). SetDoneFunc(func(buttonIndex int, buttonLabel string) { os.Exit(1) }) app.SetRoot(modal, true).SetFocus(modal) } func showMessage(app *tview.Application, text string) { modal := tview.NewModal(). SetText(text). AddButtons([]string{"OK"}). SetDoneFunc(func(buttonIndex int, buttonLabel string) { os.Exit(0) }) app.SetRoot(modal, true).SetFocus(modal).ForceDraw() }