github.com/grzegorz-zur/bm@v0.0.0-20240312214136-6fc133e3e2c0/switch.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"sort"
     8  )
     9  
    10  // Switch is a mode for switching files.
    11  type Switch struct {
    12  	editor    *Editor
    13  	query     string
    14  	paths     Paths
    15  	selection Paths
    16  	area      Area
    17  	position  Position
    18  }
    19  
    20  // Show updates mode when switched to.
    21  func (mode *Switch) Show() (err error) {
    22  	mode.query = ""
    23  	mode.paths, err = mode.read()
    24  	if err != nil {
    25  		return fmt.Errorf("error showing switch mode: %w", err)
    26  	}
    27  	sort.Sort(mode.paths)
    28  	mode.filter()
    29  	return nil
    30  }
    31  
    32  // Hide updates mode when switched from.
    33  func (mode *Switch) Hide() error {
    34  	return nil
    35  }
    36  
    37  // Key handles input events.
    38  func (mode *Switch) Key(key Key) error {
    39  	var err error
    40  	switch key {
    41  	case KeyTab:
    42  		mode.editor.SwitchMode(mode.editor.Command)
    43  	case KeyUp:
    44  		mode.moveUp()
    45  	case KeyDown:
    46  		mode.moveDown()
    47  	case KeyLeft:
    48  		mode.moveLeft()
    49  	case KeyRight:
    50  		mode.moveRight()
    51  	case KeyBackspace:
    52  		mode.backspace()
    53  		mode.filter()
    54  	case KeyEnter:
    55  		err = mode.open()
    56  		mode.editor.SwitchMode(mode.editor.Command)
    57  	}
    58  	if err != nil {
    59  		return fmt.Errorf("error handling key %v: %w", key, err)
    60  	}
    61  	return nil
    62  }
    63  
    64  // Rune handles rune input.
    65  func (mode *Switch) Rune(rune rune) error {
    66  	mode.append(rune)
    67  	mode.filter()
    68  	return nil
    69  }
    70  
    71  // Render renders mode.
    72  func (mode *Switch) Render(view *View) error {
    73  	mode.area = mode.area.Resize(view.Size).Shift(mode.position)
    74  	selection := len(mode.selection) > 0
    75  	for line := mode.area.Top; line < mode.area.Bottom; line++ {
    76  		rline := line - mode.area.Top
    77  		for column := mode.area.Left; column < mode.area.Right; column++ {
    78  			rcolumn := column - mode.area.Left
    79  			if line < len(mode.selection) {
    80  				selected := []rune(mode.selection[line])
    81  				if column < len(selected) {
    82  					view.Content[rline][rcolumn] = selected[column]
    83  				}
    84  			}
    85  			if selection && line == mode.position.Line {
    86  				view.Selection[rline][rcolumn] = true
    87  			}
    88  		}
    89  	}
    90  	status, err := os.Getwd()
    91  	if err != nil {
    92  		return fmt.Errorf("error getting working directory: %w", err)
    93  	}
    94  	view.Color = ColorBlue
    95  	view.Position = mode.position
    96  	view.Status = status
    97  	view.Prompt = string(mode.query)
    98  	view.Cursor = CursorPrompt
    99  	return nil
   100  }
   101  
   102  func (mode *Switch) filter() {
   103  	query := mode.query
   104  	mode.selection = make([]string, 0, len(mode.paths))
   105  	for _, path := range mode.paths {
   106  		if mode.match(path, query) {
   107  			mode.selection = append(mode.selection, path)
   108  		}
   109  	}
   110  	mode.position = Position{}
   111  	return
   112  }
   113  
   114  func (mode *Switch) open() error {
   115  	position := mode.position
   116  	path := mode.query
   117  	if position.Line < len(mode.selection) {
   118  		path = mode.selection[position.Line]
   119  	}
   120  	err := mode.editor.Open(path)
   121  	if err != nil {
   122  		return fmt.Errorf("error opening file %s: %w", path, err)
   123  	}
   124  	return nil
   125  }
   126  
   127  func (mode *Switch) append(rune rune) {
   128  	mode.query = mode.query + string(rune)
   129  }
   130  
   131  func (mode *Switch) backspace() {
   132  	length := len(mode.query)
   133  	if length != 0 {
   134  		mode.query = mode.query[:length-1]
   135  	}
   136  }
   137  
   138  func (mode *Switch) moveUp() {
   139  	if mode.position.Line > 0 {
   140  		mode.position.Line--
   141  	}
   142  }
   143  
   144  func (mode *Switch) moveDown() {
   145  	if mode.position.Line+1 < len(mode.selection) {
   146  		mode.position.Line++
   147  	}
   148  }
   149  
   150  func (mode *Switch) moveLeft() {
   151  	if mode.area.Left > 0 {
   152  		mode.position.Column = mode.area.Left - 1
   153  	} else {
   154  		mode.position.Column = 0
   155  	}
   156  }
   157  
   158  func (mode *Switch) moveRight() {
   159  	mode.position.Column = mode.area.Right + 1
   160  }
   161  
   162  func (mode *Switch) read() (paths []string, err error) {
   163  	work, err := os.Getwd()
   164  	if err != nil {
   165  		return paths, fmt.Errorf("error reading working directory: %w", err)
   166  	}
   167  	walker := func(path string, info os.FileInfo, err error) error {
   168  		relpath, err := filepath.Rel(work, path)
   169  		if err != nil {
   170  			return fmt.Errorf("error finding relative path %s in %s: %w", path, work, err)
   171  		}
   172  		if info == nil {
   173  			return nil
   174  		}
   175  		if info.Mode().IsRegular() {
   176  			paths = append(paths, relpath)
   177  		}
   178  		return nil
   179  	}
   180  	err = filepath.Walk(work, walker)
   181  	if err != nil {
   182  		return paths, fmt.Errorf("error walking directory %s: %w", work, err)
   183  	}
   184  	return paths, nil
   185  }
   186  
   187  func (mode *Switch) match(path, query string) bool {
   188  	if len(query) == 0 {
   189  		return true
   190  	}
   191  	j := 0
   192  	runes := []rune(query)
   193  	for _, p := range path {
   194  		q := runes[j]
   195  		if p == q {
   196  			j++
   197  		}
   198  		if j == len(query) {
   199  			return true
   200  		}
   201  	}
   202  	return false
   203  }