|
1 | 1 | package src |
2 | 2 |
|
3 | | -import ( |
4 | | - "fmt" |
5 | | - "io" |
6 | | - "io/fs" |
7 | | - "net/http" |
8 | | - "os" |
9 | | - "os/exec" |
10 | | - "path/filepath" |
11 | | - "regexp" |
12 | | - "sort" |
13 | | - "strings" |
14 | | - "syscall" |
| 3 | +import "os/exec" |
15 | 4 |
|
16 | | - "github.com/alecthomas/chroma/v2/lexers" |
17 | | - "github.com/charmbracelet/bubbles/list" |
18 | | -) |
19 | | - |
20 | | -func (m *Model) refreshPanel(idx int) { |
21 | | - p := &m.panels[idx] |
22 | | - items := []list.Item{} |
23 | | - files, err := p.vfs.ReadDir(p.currentDir) |
24 | | - if err != nil { |
25 | | - m.statusMsg = errorStyle.Render(fmt.Sprintf("Error reading directory: %v", err)) |
26 | | - return |
27 | | - } |
28 | | - gitStatus := m.getGitStatus(p) |
29 | | - sort.Slice(files, func(i, j int) bool { |
30 | | - return files[i].Name() < files[j].Name() |
31 | | - }) |
32 | | - for _, file := range files { |
33 | | - info, _ := file.Info() |
34 | | - desc := "File" |
35 | | - if file.IsDir() { |
36 | | - desc = "Directory" |
37 | | - } |
38 | | - status := gitStatus[file.Name()] |
39 | | - items = append(items, item{ |
40 | | - title: file.Name(), |
41 | | - desc: desc, |
42 | | - status: status, |
43 | | - isDir: file.IsDir(), |
44 | | - size: info.Size(), |
45 | | - modTime: info.ModTime(), |
46 | | - }) |
47 | | - } |
48 | | - p.fileList.SetItems(items) |
49 | | - m.statusMsg = successStyle.Render("Panel refreshed") |
50 | | - m.updatePreview(idx) |
51 | | - m.updateGitBranch(idx) |
52 | | -} |
53 | | - |
54 | | -func (m *Model) getGitStatus(p *panel) map[string]string { |
55 | | - statusMap := make(map[string]string) |
56 | | - if _, ok := p.vfs.(localVFS); !ok { |
57 | | - return statusMap |
58 | | - } |
59 | | - cmd := exec.Command("git", "status", "--porcelain") |
60 | | - cmd.Dir = p.currentDir |
61 | | - output, err := cmd.Output() |
62 | | - if err != nil { |
63 | | - return statusMap |
64 | | - } |
65 | | - lines := strings.Split(string(output), "\n") |
66 | | - for _, line := range lines { |
67 | | - if line == "" { |
68 | | - continue |
69 | | - } |
70 | | - fields := strings.Fields(line) |
71 | | - if len(fields) < 2 { |
72 | | - continue |
73 | | - } |
74 | | - file := fields[1] |
75 | | - st := line[:2] |
76 | | - statusMap[file] = st |
77 | | - } |
78 | | - return statusMap |
79 | | -} |
80 | | - |
81 | | -func (m *Model) updateGitBranch(idx int) { |
82 | | - p := &m.panels[idx] |
83 | | - if _, ok := p.vfs.(localVFS); !ok { |
84 | | - p.gitBranch = "" |
85 | | - return |
86 | | - } |
87 | | - cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD") |
88 | | - cmd.Dir = p.currentDir |
89 | | - output, err := cmd.Output() |
90 | | - if err != nil { |
91 | | - p.gitBranch = "" |
92 | | - return |
93 | | - } |
94 | | - p.gitBranch = strings.TrimSpace(string(output)) |
95 | | -} |
96 | | - |
97 | | -func (m *Model) updatePreview(idx int) { |
98 | | - p := &m.panels[idx] |
99 | | - selected, ok := p.fileList.SelectedItem().(item) |
100 | | - if !ok { |
101 | | - p.preview.SetContent("") |
102 | | - return |
103 | | - } |
104 | | - if selected.isDir { |
105 | | - p.preview.SetContent("Directory") |
106 | | - return |
107 | | - } |
108 | | - filePath := filepath.Join(p.currentDir, selected.title) |
109 | | - f, err := p.vfs.Open(filePath) |
110 | | - if err != nil { |
111 | | - p.preview.SetContent("Error opening file") |
112 | | - return |
113 | | - } |
114 | | - defer f.Close() |
115 | | - stat, err := f.Stat() |
116 | | - if err != nil { |
117 | | - p.preview.SetContent("Error stat file") |
118 | | - return |
119 | | - } |
120 | | - buf := make([]byte, 512) |
121 | | - _, err = f.Read(buf) |
122 | | - if err != nil && err != io.EOF { |
123 | | - p.preview.SetContent("Error reading for MIME") |
124 | | - return |
125 | | - } |
126 | | - mimeType := http.DetectContentType(buf) |
127 | | - if mimeType == "application/octet-stream" { |
128 | | - if _, ok := p.vfs.(localVFS); ok { |
129 | | - cmd := exec.Command("file", "-b", "--mime-type", filePath) |
130 | | - out, err := cmd.Output() |
131 | | - if err == nil { |
132 | | - mimeType = strings.TrimSpace(string(out)) |
133 | | - } |
134 | | - } |
135 | | - } |
136 | | - if s, ok := f.(io.Seeker); ok { |
137 | | - s.Seek(0, io.SeekStart) |
138 | | - } else { |
139 | | - f.Close() |
140 | | - f, err = p.vfs.Open(filePath) |
141 | | - if err != nil { |
142 | | - p.preview.SetContent("Error reopening file") |
143 | | - return |
144 | | - } |
145 | | - defer f.Close() |
146 | | - } |
147 | | - if strings.HasPrefix(mimeType, "image/") { |
148 | | - p.preview.SetContent("Image preview not supported in text mode. MIME: " + mimeType) |
149 | | - return |
150 | | - } |
151 | | - if !strings.HasPrefix(mimeType, "text/") { |
152 | | - p.preview.SetContent("Non-text file: " + mimeType) |
153 | | - return |
154 | | - } |
155 | | - if stat.Size() > maxFileSizeForEdit { |
156 | | - p.preview.SetContent("File too large for preview") |
157 | | - return |
158 | | - } |
159 | | - byteContent, err := io.ReadAll(f) |
160 | | - if err != nil { |
161 | | - p.preview.SetContent("Error reading file") |
162 | | - return |
163 | | - } |
164 | | - content := string(byteContent) |
165 | | - lines := strings.Split(content, "\n") |
166 | | - if len(lines) > previewLines { |
167 | | - lines = lines[:previewLines] |
168 | | - content = strings.Join(lines, "\n") + "\n..." |
169 | | - } |
170 | | - lexer := lexers.Match(selected.title) |
171 | | - if lexer == nil { |
172 | | - lexer = lexers.Fallback |
173 | | - } |
174 | | - iterator, err := lexer.Tokenise(nil, content) |
175 | | - if err != nil { |
176 | | - p.preview.SetContent(content) |
177 | | - return |
178 | | - } |
179 | | - var sb strings.Builder |
180 | | - err = chromaFormatter.Format(&sb, chromaStyle, iterator) |
181 | | - if err != nil { |
182 | | - p.preview.SetContent(content) |
183 | | - return |
184 | | - } |
185 | | - p.preview.SetContent(sb.String()) |
186 | | -} |
187 | | - |
188 | | -func (m *Model) performFuzzySearch() { |
189 | | - query := m.fuzzyInput.Value() |
190 | | - if query == "" { |
191 | | - return |
192 | | - } |
193 | | - var results []string |
194 | | - err := filepath.Walk(m.panels[m.activePanel].currentDir, func(path string, info fs.FileInfo, err error) error { |
195 | | - if err != nil { |
196 | | - return err |
197 | | - } |
198 | | - if strings.Contains(strings.ToLower(filepath.Base(path)), strings.ToLower(query)) { |
199 | | - rel, _ := filepath.Rel(m.panels[m.activePanel].currentDir, path) |
200 | | - results = append(results, rel) |
201 | | - } |
202 | | - return nil |
203 | | - }) |
204 | | - if err != nil { |
205 | | - m.statusMsg = errorStyle.Render(err.Error()) |
206 | | - } |
207 | | - m.fuzzyResults = results |
208 | | - items := []list.Item{} |
209 | | - for _, res := range results { |
210 | | - items = append(items, item{title: res}) |
211 | | - } |
212 | | - m.panels[m.activePanel].fileList.SetItems(items) |
213 | | -} |
214 | | - |
215 | | -func (m *Model) performBulkRename() { |
216 | | - re, err := regexp.Compile(m.bulkRenameFrom) |
217 | | - if err != nil { |
218 | | - m.statusMsg = errorStyle.Render("Invalid regex") |
219 | | - return |
220 | | - } |
221 | | - for file := range m.panels[m.activePanel].selectedFiles { |
222 | | - newName := re.ReplaceAllString(filepath.Base(file), m.bulkRenameTo) |
223 | | - newPath := filepath.Join(filepath.Dir(file), newName) |
224 | | - err := os.Rename(file, newPath) |
225 | | - if err != nil { |
226 | | - m.statusMsg += errorStyle.Render(fmt.Sprintf("Rename failed for %s: %v\n", file, err)) |
227 | | - } |
228 | | - } |
229 | | - m.panels[m.activePanel].selectedFiles = make(map[string]bool) |
230 | | - m.refreshPanel(m.activePanel) |
231 | | - m.bulkRenameFrom = "" |
232 | | - m.bulkRenameTo = "" |
233 | | -} |
234 | | - |
235 | | -func (m *Model) suspend() { |
236 | | - pid := os.Getpid() |
237 | | - syscall.Kill(pid, syscall.SIGTSTP) |
238 | | - m.refreshPanel(m.activePanel) |
239 | | -} |
240 | | - |
241 | | -func (m *Model) openSubShell() { |
242 | | - m.subShell = true |
243 | | - cmd := exec.Command(os.Getenv("SHELL")) |
244 | | - cmd.Stdin = os.Stdin |
245 | | - cmd.Stdout = os.Stdout |
246 | | - cmd.Stderr = os.Stderr |
247 | | - cmd.Dir = m.panels[m.activePanel].currentDir |
248 | | - cmd.Run() |
249 | | - m.subShell = false |
250 | | - m.refreshPanel(m.activePanel) |
| 5 | +// buildCmd is a thin wrapper so we can intercept exec calls in tests. |
| 6 | +func buildCmd(name string, args ...string) *exec.Cmd { |
| 7 | + return exec.Command(name, args...) |
251 | 8 | } |
0 commit comments