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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Changed

- File manager `l` on a file now opens it: text files open in `$EDITOR`, binary/unreadable files open with the platform default (`Start-Process` on Windows, `xdg-open` on Linux, `open` on macOS)

## 0.11.0 - 2026-02-18

### Added
Expand Down
116 changes: 116 additions & 0 deletions mshell/FileManager.go
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,12 @@ func (fm *FileManager) enterSelected() {
}
entry := fm.entries[fm.cursor]
if !entry.IsDir() {
switch runtime.GOOS {
case "windows":
fm.openFileWindows(entry)
case "linux", "darwin":
fm.openFileUnix(entry)
}
return
}
newDir := filepath.Join(fm.currentDir, entry.Name())
Expand Down Expand Up @@ -1251,6 +1257,116 @@ func (fm *FileManager) openEditor() {
fm.adjustScroll()
}

func isBinaryFile(path string) bool {
if strings.EqualFold(filepath.Ext(path), ".pdf") {
return true
}
f, err := os.Open(path)
if err != nil {
return true
}
defer f.Close()
probe := make([]byte, 512)
n, _ := f.Read(probe)
return n > 0 && bytes.ContainsRune(probe[:n], 0)
}

func (fm *FileManager) openFileWindows(entry os.DirEntry) {
filePath := filepath.Join(fm.currentDir, entry.Name())

editor := os.Getenv("EDITOR")
if editor != "" && !isBinaryFile(filePath) {
// Open text file in editor, same as 'e' binding
fm.ttyOut.WriteString("\033[?25h\033[?1049l")
term.Restore(fm.stdInFd, &fm.oldState)

cmd := exec.Command(editor, filePath)
cmd.Dir = fm.currentDir
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()

// Re-enter raw mode and alternate buffer
newState, _ := term.MakeRaw(fm.stdInFd)
if newState != nil {
fm.oldState = *newState
}
fm.ttyOut.WriteString("\033[?1049h\033[?25l")

cols, rows, sizeErr := term.GetSize(int(fm.ttyOut.Fd()))
if sizeErr == nil {
fm.rows = rows
fm.cols = cols
}

fm.loadDirectory()
fm.clampCursor()
fm.adjustScroll()

if err == nil {
return
}
// Editor failed; fall through to Start-Process
}

// Binary file, no $EDITOR, or editor failed: open with Windows default app
escapedPath := strings.ReplaceAll(filePath, "'", "''")
psCmd := "Start-Process -FilePath '" + escapedPath + "'"
cmd := exec.Command("powershell.exe", "-NoProfile", "-Command", psCmd)
cmd.Stdin = nil
cmd.Run()
}

func (fm *FileManager) openFileUnix(entry os.DirEntry) {
filePath := filepath.Join(fm.currentDir, entry.Name())

editor := os.Getenv("EDITOR")
if editor != "" && !isBinaryFile(filePath) {
// Open text file in editor, same as 'e' binding
fm.ttyOut.WriteString("\033[?25h\033[?1049l")
term.Restore(fm.stdInFd, &fm.oldState)

cmd := exec.Command(editor, filePath)
cmd.Dir = fm.currentDir
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()

// Re-enter raw mode and alternate buffer
newState, _ := term.MakeRaw(fm.stdInFd)
if newState != nil {
fm.oldState = *newState
}
fm.ttyOut.WriteString("\033[?1049h\033[?25l")

cols, rows, sizeErr := term.GetSize(int(fm.ttyOut.Fd()))
if sizeErr == nil {
fm.rows = rows
fm.cols = cols
}

fm.loadDirectory()
fm.clampCursor()
fm.adjustScroll()

if err == nil {
return
}
// Editor failed; fall through to xdg-open/open
}

// Binary file, no $EDITOR, or editor failed: open with system default
opener := "xdg-open"
if runtime.GOOS == "darwin" {
opener = "open"
}
cmd := exec.Command(opener, filePath)
cmd.Stdin = nil
cmd.Start()
}

// Clipboard actions

func (fm *FileManager) clipboardCut() {
Expand Down