Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/reborn #113

Merged
merged 32 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f37d59f
chore: move teacup bubbles here
mistakenelf Mar 8, 2024
5211c5c
feat: render file tree
mistakenelf Mar 10, 2024
2786382
chore: cleanup
mistakenelf Mar 10, 2024
0d4a608
chore: rename
mistakenelf Mar 10, 2024
662c6ed
chore: filetree scrolling and resizing
mistakenelf Mar 10, 2024
f62ef7a
chore: cleanup
mistakenelf Mar 10, 2024
33c3f44
feat: use flags for config
mistakenelf Mar 11, 2024
e9b3f55
feat: set start dir
mistakenelf Mar 11, 2024
a8584e7
feat: page up and down
mistakenelf Mar 11, 2024
3a25204
Merge branch 'feature/reborn' of https://github.com/mistakenelf/fm in…
mistakenelf Mar 12, 2024
299502c
feat: more file navigation
mistakenelf Mar 12, 2024
94bac41
feat: more tree commands and status messages
mistakenelf Mar 12, 2024
358868d
feat: more updates
mistakenelf Mar 13, 2024
6c12966
feat: viewport update fix
mistakenelf Mar 14, 2024
23e6e5f
fix: ensure flags are all working
mistakenelf Mar 14, 2024
c702b4f
feat: set syntax theme
mistakenelf Mar 14, 2024
43c3637
feat: open in editor
mistakenelf Mar 14, 2024
767d605
chore: move to view
mistakenelf Mar 15, 2024
7e1b95d
feat: WIP create file
mistakenelf Mar 15, 2024
b53992d
feat: create file and directory
mistakenelf Mar 15, 2024
32bf683
fix: text input update too soon
mistakenelf Mar 16, 2024
dd6434f
feat: update keybinding help
mistakenelf Mar 16, 2024
de082f7
feat: file icons
mistakenelf Mar 16, 2024
8d4f4b9
chore: cleanup
mistakenelf Mar 18, 2024
63123a6
chore: update readme
mistakenelf Mar 23, 2024
e0dce84
feat: lots of improvements
mistakenelf Mar 23, 2024
e629e0d
fix: dont reset dir if not moving
mistakenelf Mar 23, 2024
f48f050
feat: WIP move directory items
mistakenelf Mar 23, 2024
aaf924b
chore: update deps
mistakenelf Mar 26, 2024
bd1f42c
feat: move directory items
mistakenelf Mar 26, 2024
3065ecb
fix: reset pane and disable secondary tree
mistakenelf Mar 26, 2024
890b5ac
feat: created shared color library polish
mistakenelf Mar 26, 2024
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
4 changes: 0 additions & 4 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ linters:
disable-all: true
enable:
- bodyclose
- deadcode
- depguard
- dogsled
- errcheck
- exportloopref
Expand All @@ -24,13 +22,11 @@ linters:
- nolintlint
- rowserrcheck
- staticcheck
- structcheck
- stylecheck
- typecheck
- unconvert
- unparam
- unused
- varcheck
- whitespace
- wastedassign
- nilerr
Expand Down
52 changes: 14 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ A terminal based file manager
- [Glamour](https://github.com/charmbracelet/glamour)
- [Chroma](https://github.com/alecthomas/chroma)
- [Cobra](https://github.com/spf13/cobra)
- [Teacup](https://github.com/mistakenelf/teacup)

## Installation

Expand Down Expand Up @@ -62,8 +61,7 @@ paru -S fm-bin

## Features

- Double pane layout
- File icons
- File icons (requires nerd font)
- Layout adjusts to terminal resize
- Syntax highlighting for source code with customizable themes using styles from [chroma](https://swapoff.org/chroma/playground/) (dracula, monokai etc.)
- Render pretty markdown
Expand Down Expand Up @@ -95,26 +93,32 @@ paru -S fm-bin
- `fm update` will update fm to the latest version
- `fm --start-dir=/some/start/dir` will start fm in the specified directory
- `fm --selection-path=/tmp/tmpfile` will write the selected items path to the selection path when pressing <kbd>E</kbd> and exit fm
- `fm --start-dir=/some/dir` start fm at a specific directory
- `fm --enable-logging=true` start fm with logging enabled
- `fm --pretty-markdown=true` render markdown using glamour to make it look nice
- `fm --theme=default` set the theme of fm
- `fm --show-icons=false` set whether to show icons or not
- `fm --syntax-theme=dracula` sets the syntax theme to render code with

## Navigation

| Key | Description |
| --------------------- | ---------------------------------------------------------- |
| <kbd>h or left</kbd> | Paginate to the left |
| <kbd>j or down</kbd> | Move down in the file tree or scroll pane down |
| <kbd>h or left</kbd> | Go to previous directory |
| <kbd>j or down</kbd> | Move down in the file tree or scroll pane down |
| <kbd>k or up</kbd> | Move up in the file tree or scroll pane up |
| <kbd>l or right</kbd> | Paginate to the right |
| <kbd>l or right</kbd> | Open file or directory |
| <kbd>G</kbd> | Jump to bottom of file tree or pane |
| <kbd>g</kbd> | Jump to top of file tree or pane |
| <kbd>~</kbd> | Go to home directory |
| <kbd>R</kbd> | Go to the root directory |
| <kbd>/</kbd> | Go to the root directory |
| <kbd>.</kbd> | Toggle hidden files and directories |
| <kbd>ctrl+c</kbd> | Exit |
| <kbd>q</kbd> | Exit if command bar is not open |
| <kbd>tab</kbd> | Toggle between panes |
| <kbd>esc</kbd> | Blur filetree input |
| <kbd>z</kbd> | Create a zip file of the currently selected directory item |
| <kbd>u</kbd> | Unzip a zip file |
| <kbd>esc</kbd> | Reset app state and show help screen |
| <kbd>Z</kbd> | Create a zip file of the currently selected directory item |
| <kbd>U</kbd> | Unzip a zip file |
| <kbd>c</kbd> | Create a copy of a file or directory |
| <kbd>x</kbd> | Delete the currently selected file or directory |
| <kbd>n</kbd> | Create a new file in the current directory |
Expand All @@ -127,30 +131,6 @@ paru -S fm-bin
| <kbd>?</kbd> | Toggle filetree full help menu |
| <kbd>ctrl+r</kbd> | Reload config |

## Configuration

A config file will be generated when you first run `fm`. Depending on your operating system it can be found in one of the following locations:

- macOS: ~/Library/Application\ Support/fm/config.yml
- Linux: ~/.config/fm/config.yml
- Windows: C:\Users\me\AppData\Roaming\fm\config.yml

It will include the following default settings:

```yml
settings:
borderless: false
enable_logging: false
pretty_markdown: true
show_icons: true
start_dir: .
theme:
app_theme: default
syntax_theme:
dark: dracula
light: pygments
```

## Local Development

Follow the instructions below to get setup for local development
Expand All @@ -172,7 +152,3 @@ make
```sh
make build
```

## Credit

- Thank you to this repo https://github.com/Yash-Handa/logo-ls for the icons
61 changes: 45 additions & 16 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@ import (
"log"
"os"

"github.com/mistakenelf/fm/internal/config"
"github.com/mistakenelf/fm/internal/tui"

tea "github.com/charmbracelet/bubbletea"
"github.com/spf13/cobra"

"github.com/mistakenelf/fm/filesystem"
"github.com/mistakenelf/fm/internal/theme"
"github.com/mistakenelf/fm/internal/tui"
)

var rootCmd = &cobra.Command{
Use: "fm",
Short: "FM is a simple, configurable, and fun to use file manager",
Version: "0.16.0",
Version: "1.0.0",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
startDir, err := cmd.Flags().GetString("start-dir")
Expand All @@ -28,13 +29,33 @@ var rootCmd = &cobra.Command{
log.Fatal(err)
}

cfg, err := config.ParseConfig()
enableLogging, err := cmd.Flags().GetBool("enable-logging")
if err != nil {
log.Fatal(err)
}

prettyMarkdown, err := cmd.Flags().GetBool("pretty-markdown")
if err != nil {
log.Fatal(err)
}

applicationTheme, err := cmd.Flags().GetString("theme")
if err != nil {
log.Fatal(err)
}

showIcons, err := cmd.Flags().GetBool("show-icons")
if err != nil {
log.Fatal(err)
}

syntaxTheme, err := cmd.Flags().GetString("syntax-theme")
if err != nil {
log.Fatal(err)
}

// If logging is enabled, logs will be output to debug.log.
if cfg.Settings.EnableLogging {
if enableLogging {
f, err := tea.LogToFile("debug.log", "debug")
if err != nil {
log.Fatal(err)
Expand All @@ -47,18 +68,21 @@ var rootCmd = &cobra.Command{
}()
}

if startDir == "" {
startDir = cfg.Settings.StartDir
}
appTheme := theme.GetTheme(applicationTheme)

m := tui.New(startDir, selectionPath)
var opts []tea.ProgramOption
cfg := tui.Config{
StartDir: startDir,
SelectionPath: selectionPath,
EnableLogging: enableLogging,
PrettyMarkdown: prettyMarkdown,
Theme: appTheme,
ShowIcons: showIcons,
SyntaxTheme: syntaxTheme,
}

// Always append alt screen program option.
opts = append(opts, tea.WithAltScreen())
m := tui.New(cfg)

// Initialize and start app.
p := tea.NewProgram(m, opts...)
p := tea.NewProgram(m, tea.WithAltScreen())
if _, err := p.Run(); err != nil {
log.Fatal("Failed to start fm", err)
os.Exit(1)
Expand All @@ -70,7 +94,12 @@ var rootCmd = &cobra.Command{
func Execute() {
rootCmd.AddCommand(updateCmd)
rootCmd.PersistentFlags().String("selection-path", "", "Path to write to file on open.")
rootCmd.PersistentFlags().String("start-dir", "", "Starting directory for FM")
rootCmd.PersistentFlags().String("start-dir", filesystem.CurrentDirectory, "Starting directory for FM")
rootCmd.PersistentFlags().Bool("enable-logging", false, "Enable logging for FM")
rootCmd.PersistentFlags().Bool("pretty-markdown", true, "Render markdown to look nice")
rootCmd.PersistentFlags().String("theme", "default", "Application theme")
rootCmd.PersistentFlags().Bool("show-icons", true, "Show icons")
rootCmd.PersistentFlags().String("syntax-theme", "dracula", "Set syntax theme for file output")

if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
Expand Down
169 changes: 169 additions & 0 deletions code/code.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Package code implements a code bubble which renders syntax highlighted
// source code based on a filename.
package code

import (
"bytes"
"fmt"
"path/filepath"
"time"

"github.com/alecthomas/chroma/v2/quick"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"

"github.com/mistakenelf/fm/filesystem"
"github.com/mistakenelf/fm/polish"
)

type syntaxMsg string
type statusMessageTimeoutMsg struct{}
type errorMsg string

// Model represents the properties of a code bubble.
type Model struct {
Viewport viewport.Model
Filename string
Content string
SyntaxTheme string
StatusMessage string
StatusMessageLifetime time.Duration
statusMessageTimer *time.Timer
ViewportDisabled bool
}

// Highlight returns a syntax highlighted string of text.
func Highlight(content, extension, syntaxTheme string) (string, error) {
buf := new(bytes.Buffer)
if err := quick.Highlight(buf, content, extension, "terminal256", syntaxTheme); err != nil {
return "", fmt.Errorf("%w", err)
}

return buf.String(), nil
}

func readFileContentCmd(fileName, syntaxTheme string) tea.Cmd {
return func() tea.Msg {
content, err := filesystem.ReadFileContent(fileName)
if err != nil {
return errorMsg(err.Error())
}

highlightedContent, err := Highlight(content, filepath.Ext(fileName), syntaxTheme)
if err != nil {
return errorMsg(err.Error())
}

return syntaxMsg(highlightedContent)
}
}

// NewStatusMessage sets a new status message, which will show for a limited
// amount of time.
func (m *Model) NewStatusMessageCmd(s string) tea.Cmd {
m.StatusMessage = s
if m.statusMessageTimer != nil {
m.statusMessageTimer.Stop()
}

m.statusMessageTimer = time.NewTimer(m.StatusMessageLifetime)

// Wait for timeout
return func() tea.Msg {
<-m.statusMessageTimer.C
return statusMessageTimeoutMsg{}
}
}

// SetFileName sets current file to highlight.
func (m *Model) SetFileNameCmd(filename string) tea.Cmd {
m.Filename = filename

return readFileContentCmd(filename, m.SyntaxTheme)
}

// New creates a new instance of code.
func New() Model {
viewPort := viewport.New(0, 0)

return Model{
Viewport: viewPort,
SyntaxTheme: "dracula",
StatusMessage: "",
StatusMessageLifetime: time.Second,
}
}

// Init initializes the code bubble.
func (m Model) Init() tea.Cmd {
return nil
}

// SetSyntaxTheme sets the syntax theme of the rendered code.
func (m *Model) SetSyntaxTheme(theme string) {
m.SyntaxTheme = theme
}

// SetSize sets the size of the bubble.
func (m *Model) SetSize(w, h int) {
m.Viewport.Width = w
m.Viewport.Height = h
}

// GotoTop jumps to the top of the viewport.
func (m *Model) GotoTop() {
m.Viewport.GotoTop()
}

// GotoBottom jumps to the bottom of the viewport.
func (m *Model) GotoBottom() {
m.Viewport.GotoBottom()
}

// SetViewportDisabled toggles the state of the viewport.
func (m *Model) SetViewportDisabled(disabled bool) {
m.ViewportDisabled = disabled
}

// Update handles updating the UI of the code bubble.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)

switch msg := msg.(type) {
case syntaxMsg:
m.Filename = ""
m.Content = lipgloss.NewStyle().
Width(m.Viewport.Width).
Height(m.Viewport.Height).
Render(string(msg))

m.Viewport.SetContent(m.Content)

case statusMessageTimeoutMsg:
m.StatusMessage = ""
case errorMsg:
m.Filename = ""
cmds = append(cmds, m.NewStatusMessageCmd(
lipgloss.NewStyle().
Foreground(polish.Colors.Red600).
Bold(true).
Render(string(msg)),
))
}

if !m.ViewportDisabled {
m.Viewport, cmd = m.Viewport.Update(msg)
cmds = append(cmds, cmd)
}

return m, tea.Batch(cmds...)
}

// View returns a string representation of the code bubble.
func (m Model) View() string {
return m.Viewport.View()
}
Loading