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 Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Binary name
BINARY_NAME=lazyprisma
VERSION ?= 0.3.0
VERSION ?= 0.3.1

# Directories
BUILD_DIR=build
Expand Down
26 changes: 25 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
)

const (
Version = "v0.3.0"
Version = "v0.3.1"
Developer = "DokaLab"
)

Expand Down Expand Up @@ -112,6 +112,30 @@ func main() {
tuiApp.RegisterPanel(output)
tuiApp.RegisterPanel(statusbar)

// Create and wire controllers
gui := tuiApp.GetGui()

migrationsController := app.NewMigrationsController(
tuiApp, gui, migrationsCtx, output,
tuiApp.OpenModal, tuiApp.CloseModal,
tuiApp.RunStreamingCommand,
)
generateController := app.NewGenerateController(
tuiApp, gui, output,
tuiApp.OpenModal,
tuiApp.RunStreamingCommand,
)
studioController := app.NewStudioController(
tuiApp, gui, output,
tuiApp.OpenModal,
)
clipboardController := app.NewClipboardController(
tuiApp, gui, migrationsCtx,
tuiApp.OpenModal, tuiApp.CloseModal,
)

tuiApp.SetControllers(migrationsController, generateController, studioController, clipboardController)

// Register keybindings
if err := tuiApp.RegisterKeybindings(); err != nil {
fmt.Fprintf(os.Stderr, tr.ErrorFailedRegisterKeybindings, err)
Expand Down
54 changes: 34 additions & 20 deletions pkg/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"sync/atomic"
"time"

"github.com/dokadev/lazyprisma/pkg/commands"
"github.com/dokadev/lazyprisma/pkg/common"
"github.com/dokadev/lazyprisma/pkg/gui/context"
"github.com/dokadev/lazyprisma/pkg/i18n"
Expand Down Expand Up @@ -36,9 +35,11 @@ type App struct {
spinnerFrame atomic.Uint32 // Current spinner frame index (0-3)
stopSpinnerCh chan struct{} // Channel to stop spinner goroutine

// Studio process management
studioCmd *commands.Command // Running studio command
studioRunning bool // True if studio is running
// Controllers
migrationsController *MigrationsController
generateController *GenerateController
studioController *StudioController
clipboardController *ClipboardController
}

type AppConfig struct {
Expand Down Expand Up @@ -78,13 +79,23 @@ func NewApp(config AppConfig) (*App, error) {
return app, nil
}

// SetControllers wires the extracted controllers into the App.
func (a *App) SetControllers(mc *MigrationsController, gc *GenerateController, sc *StudioController, cc *ClipboardController) {
a.migrationsController = mc
a.generateController = gc
a.studioController = sc
a.clipboardController = cc
}

func (a *App) Run() error {
defer a.g.Close()
defer close(a.stopSpinnerCh) // Stop spinner goroutine
defer func() {
// Kill studio process if running
if a.studioCmd != nil {
a.studioCmd.Kill()
if a.studioController != nil {
if cmd := a.studioController.GetStudioCmd(); cmd != nil {
cmd.Kill()
}
}
}()

Expand Down Expand Up @@ -119,7 +130,10 @@ func (a *App) StatusBarState() context.StatusBarState {
return a.spinnerFrame.Load()
},
IsStudioRunning: func() bool {
return a.studioRunning
if a.studioController != nil {
return a.studioController.IsStudioRunning()
}
return false
},
GetCommandName: func() string {
if val := a.runningCommandName.Load(); val != nil {
Expand Down Expand Up @@ -173,9 +187,9 @@ func (a *App) GetCurrentPanel() Panel {
return nil
}

// tryStartCommand attempts to start a command execution
// Returns true if command can start, false if another command is already running
func (a *App) tryStartCommand(commandName string) bool {
// TryStartCommand attempts to start a command execution.
// Returns true if command can start, false if another command is already running.
func (a *App) TryStartCommand(commandName string) bool {
// CompareAndSwap atomically: if false, set to true and return true
// if already true, return false
if a.commandRunning.CompareAndSwap(false, true) {
Expand All @@ -185,15 +199,15 @@ func (a *App) tryStartCommand(commandName string) bool {
return false
}

// finishCommand marks command execution as complete
func (a *App) finishCommand() {
// FinishCommand marks command execution as complete.
func (a *App) FinishCommand() {
a.runningCommandName.Store("")
a.commandRunning.Store(false)
a.spinnerFrame.Store(0) // Reset spinner to first frame
}

// logCommandBlocked logs a message when command execution is blocked
func (a *App) logCommandBlocked(commandName string) {
// LogCommandBlocked logs a message when command execution is blocked.
func (a *App) LogCommandBlocked(commandName string) {
a.g.Update(func(g *gocui.Gui) error {
if outputPanel, ok := a.panels[ViewOutputs].(*context.OutputContext); ok {
runningTask := ""
Expand Down Expand Up @@ -456,16 +470,16 @@ func (a *App) registerMouseWheelBindings() {
// RefreshAll refreshes all panels asynchronously
func (a *App) RefreshAll(onComplete ...func()) bool {
// Try to start command - if another command is running, block
if !a.tryStartCommand("Refresh All") {
a.logCommandBlocked("Refresh All")
if !a.TryStartCommand("Refresh All") {
a.LogCommandBlocked("Refresh All")
return false
}

// Run refresh in background to avoid blocking UI
go func() {
defer a.finishCommand() // Always mark command as complete
defer a.FinishCommand() // Always mark command as complete

a.refreshPanels()
a.RefreshPanels()

// Update UI on main thread (thread-safe)
a.g.Update(func(g *gocui.Gui) error {
Expand All @@ -485,8 +499,8 @@ func (a *App) RefreshAll(onComplete ...func()) bool {
return true
}

// refreshPanels refreshes all panels (blocking, internal)
func (a *App) refreshPanels() {
// RefreshPanels refreshes all panels (blocking, internal).
func (a *App) RefreshPanels() {
// Refresh workspace panel
if workspaceCtx, ok := a.panels[ViewWorkspace].(*context.WorkspaceContext); ok {
workspaceCtx.Refresh()
Expand Down
6 changes: 6 additions & 0 deletions pkg/app/base_modal.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ func (b *BaseModal) SetupView(name string, x0, y0, x1, y1 int, zIndex byte, titl
return v, isNew, nil
}

// AcceptsTextInput returns false by default (most modals don't accept text input).
func (b *BaseModal) AcceptsTextInput() bool { return false }

// ClosesOnEnter returns false by default (most modals handle Enter via HandleKey).
func (b *BaseModal) ClosesOnEnter() bool { return false }

// OnClose deletes the modal's primary view
func (b *BaseModal) OnClose() {
b.g.DeleteView(b.id)
Expand Down
79 changes: 52 additions & 27 deletions pkg/app/clipboard_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,62 @@ import (
"fmt"

"github.com/dokadev/lazyprisma/pkg/gui/context"
"github.com/dokadev/lazyprisma/pkg/gui/types"
"github.com/jesseduffield/gocui"
)

// CopyMigrationInfo copies migration info to clipboard
func (a *App) CopyMigrationInfo() {
// Get migrations panel
migrationsPanel, ok := a.panels[ViewMigrations].(*context.MigrationsContext)
if !ok {
return
// ClipboardController handles clipboard-related operations.
type ClipboardController struct {
c types.IControllerHost
g *gocui.Gui
migrationsCtx *context.MigrationsContext
openModal func(Modal)
closeModal func()
}

// NewClipboardController creates a new ClipboardController.
func NewClipboardController(
c types.IControllerHost,
g *gocui.Gui,
migrationsCtx *context.MigrationsContext,
openModal func(Modal),
closeModal func(),
) *ClipboardController {
return &ClipboardController{
c: c,
g: g,
migrationsCtx: migrationsCtx,
openModal: openModal,
closeModal: closeModal,
}
}

// CopyMigrationInfo copies migration info to clipboard
func (cc *ClipboardController) CopyMigrationInfo() {
tr := cc.c.GetTranslationSet()

// Get selected migration
selected := migrationsPanel.GetSelectedMigration()
selected := cc.migrationsCtx.GetSelectedMigration()
if selected == nil {
return
}

items := []ListModalItem{
{
Label: a.Tr.ListItemCopyName,
Label: tr.ListItemCopyName,
Description: selected.Name,
OnSelect: func() error {
a.CloseModal()
a.copyTextToClipboard(selected.Name, a.Tr.CopyLabelMigrationName)
cc.closeModal()
cc.copyTextToClipboard(selected.Name, tr.CopyLabelMigrationName)
return nil
},
},
{
Label: a.Tr.ListItemCopyPath,
Label: tr.ListItemCopyPath,
Description: selected.Path,
OnSelect: func() error {
a.CloseModal()
a.copyTextToClipboard(selected.Path, a.Tr.CopyLabelMigrationPath)
cc.closeModal()
cc.copyTextToClipboard(selected.Path, tr.CopyLabelMigrationPath)
return nil
},
},
Expand All @@ -44,39 +68,40 @@ func (a *App) CopyMigrationInfo() {
// If it has a checksum, allow copying it
if selected.Checksum != "" {
items = append(items, ListModalItem{
Label: a.Tr.ListItemCopyChecksum,
Label: tr.ListItemCopyChecksum,
Description: selected.Checksum,
OnSelect: func() error {
a.CloseModal()
a.copyTextToClipboard(selected.Checksum, a.Tr.CopyLabelChecksum)
cc.closeModal()
cc.copyTextToClipboard(selected.Checksum, tr.CopyLabelChecksum)
return nil
},
})
}

modal := NewListModal(a.g, a.Tr, a.Tr.ModalTitleCopyToClipboard, items,
modal := NewListModal(cc.g, tr, tr.ModalTitleCopyToClipboard, items,
func() {
a.CloseModal()
cc.closeModal()
},
).WithStyle(MessageModalStyle{TitleColor: ColorCyan, BorderColor: ColorCyan})

a.OpenModal(modal)
cc.openModal(modal)
}

func (a *App) copyTextToClipboard(text, label string) {
func (cc *ClipboardController) copyTextToClipboard(text, label string) {
tr := cc.c.GetTranslationSet()

if err := CopyToClipboard(text); err != nil {
modal := NewMessageModal(a.g, a.Tr, a.Tr.ModalTitleClipboardError,
a.Tr.ModalMsgFailedCopyClipboard,
modal := NewMessageModal(cc.g, tr, tr.ModalTitleClipboardError,
tr.ModalMsgFailedCopyClipboard,
err.Error(),
).WithStyle(MessageModalStyle{TitleColor: ColorRed, BorderColor: ColorRed})
a.OpenModal(modal)
cc.openModal(modal)
return
}

// Show toast/notification via modal for now
// Ideally we would have a toast system
modal := NewMessageModal(a.g, a.Tr, a.Tr.ModalTitleCopied,
fmt.Sprintf(a.Tr.ModalMsgCopiedToClipboard, label),
modal := NewMessageModal(cc.g, tr, tr.ModalTitleCopied,
fmt.Sprintf(tr.ModalMsgCopiedToClipboard, label),
).WithStyle(MessageModalStyle{TitleColor: ColorGreen, BorderColor: ColorGreen})
a.OpenModal(modal)
cc.openModal(modal)
}
Loading