diff --git a/internal/config/config.go b/internal/config/config.go index ef2bf01..796644e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -70,6 +70,8 @@ type BehaviorConfig struct { ConfirmOverwrite bool `toml:"confirm_overwrite"` CalculateDirSizeOnSpace bool `toml:"calculate_dir_size_on_space"` FollowSymlinks bool `toml:"follow_symlinks"` + AutoRefresh bool `toml:"auto_refresh"` + AutoRefreshInterval int `toml:"auto_refresh_interval"` } func Default() Config { @@ -111,6 +113,8 @@ func Default() Config { ConfirmOverwrite: true, CalculateDirSizeOnSpace: true, FollowSymlinks: false, + AutoRefresh: true, + AutoRefreshInterval: 5, }, } } @@ -205,6 +209,14 @@ func (c *Config) Validate() error { if c.UI.CenterWidthPercent < 20 || c.UI.CenterWidthPercent > 60 { return errors.New("ui.center_width_percent must be between 20 and 60") } + if c.Behavior.AutoRefresh { + if c.Behavior.AutoRefreshInterval < 1 { + c.Behavior.AutoRefreshInterval = 5 + } + if c.Behavior.AutoRefreshInterval > 60 { + return errors.New("behavior.auto_refresh_interval must be between 1 and 60") + } + } switch strings.ToLower(strings.TrimSpace(c.Browser.Sort.By)) { case "", "name": c.Browser.Sort.By = "name" diff --git a/internal/ui/model.go b/internal/ui/model.go index bd538c0..4a5dbc6 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -167,6 +167,7 @@ type copyDoneMsg struct { type dismissNoticeMsg struct{} type dismissYankFlashMsg struct{} +type tickMsg struct{} type externalOpenMsg struct { path string err error @@ -333,6 +334,9 @@ func NewModel(cfg config.Config, configPath string) (Model, error) { } func (m Model) Init() tea.Cmd { + if m.cfg.Behavior.AutoRefresh { + return tea.Batch(m.loadPreviewCmd(), autoRefreshTickCmd(m.cfg.Behavior.AutoRefreshInterval)) + } return m.loadPreviewCmd() } @@ -919,6 +923,20 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.syncPreviewContent() return m, nil + case tickMsg: + if !m.cfg.Behavior.AutoRefresh || + m.busy || + m.copyJob != nil || + m.archiveJob != nil || + m.modal.kind != modalNone || + m.filterMode || + m.cursorMode || m.visualMode || + m.viewMode { + return m, autoRefreshTickCmd(m.cfg.Behavior.AutoRefreshInterval) + } + m.autoRefreshPanes() + return m, tea.Batch(autoRefreshTickCmd(m.cfg.Behavior.AutoRefreshInterval), m.loadPreviewCmd()) + case externalOpenMsg: if msg.err != nil { m.status = fmt.Sprintf("Open failed: %v", msg.err) @@ -1768,6 +1786,21 @@ func (m *Model) refreshAllPanes(status string) (tea.Model, tea.Cmd) { return m, m.loadPreviewCmd() } +func (m *Model) autoRefreshPanes() { + for _, id := range []PaneID{PaneLeft, PaneRight} { + pane := m.paneByID(id) + if pane.InRemote() || pane.InArchive() { + continue + } + if name := selectedName(pane); name != "" { + pane.SaveCursor(pane.Path, name) + } + if err := m.reloadPane(id, pane.LoadCursor(pane.Path)); err != nil { + log.Printf("[REFRESH] pane=%s path=%s err=%v", id, pane.Path, err) + } + } +} + func (m *Model) moveCursor(delta int) { // When a filter query is active on this pane, move through filtered entries // only, so the cursor always lands on a matching item. @@ -4867,6 +4900,12 @@ func dismissYankFlashCmd(delay time.Duration) tea.Cmd { }) } +func autoRefreshTickCmd(seconds int) tea.Cmd { + return tea.Tick(time.Duration(seconds)*time.Second, func(time.Time) tea.Msg { + return tickMsg{} + }) +} + func (m *Model) startCopyJob(kind fileOpKind, sourcePaths []string, targetDir string, overwrite bool, stats vfs.TransferStats) tea.Cmd { m.nextCopyJob++ jobID := m.nextCopyJob diff --git a/vcom.toml b/vcom.toml index 0050ba1..ea51e44 100644 --- a/vcom.toml +++ b/vcom.toml @@ -41,3 +41,5 @@ confirm_delete = true confirm_overwrite = true calculate_dir_size_on_space = true follow_symlinks = false +auto_refresh = true +auto_refresh_interval = 5