initial commit
This commit is contained in:
commit
4b266c97e9
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
lf
|
||||||
|
tags
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2016 Gökçehan Kara
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
12
Makefile
Normal file
12
Makefile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
all: build
|
||||||
|
|
||||||
|
build:
|
||||||
|
CGO_ENABLED=0 go build -ldflags '-s'
|
||||||
|
|
||||||
|
install:
|
||||||
|
mv lf $(GOPATH)/bin
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test
|
||||||
|
|
||||||
|
.PHONY: all test
|
69
README.md
Normal file
69
README.md
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# LF
|
||||||
|
|
||||||
|
> Please note that this is an experimental file manager.
|
||||||
|
> One of the most dangerous pieces of software you can play with.
|
||||||
|
> You may accidentally lose your files or worse so use at your own risk.
|
||||||
|
|
||||||
|
> Likewise it is a work in progress.
|
||||||
|
> You will most likely come across some shameful bugs.
|
||||||
|
> Also some essentials may not have been implemented yet.
|
||||||
|
|
||||||
|
`lf` (as in "list files") is a terminal file manager written in Go.
|
||||||
|
It is heavily inspired by ranger with some missing and extra features.
|
||||||
|
Some of the missing features are deliberately ommited
|
||||||
|
since it is better if they are handled by external tools.
|
||||||
|
|
||||||
|
![multicol-screenshot](http://i.imgur.com/DaTUenu.png)
|
||||||
|
![singlecol-screenshot](http://i.imgur.com/p95xzUj.png)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- no external runtime dependencies (except for terminfo database)
|
||||||
|
- fast startup and low memory footprint (due to native code and static binaries)
|
||||||
|
- server/client architecture to share selection between multiple instances
|
||||||
|
- custom commands as shell scripts (hence any other language as well)
|
||||||
|
- sync (waiting and skipping) and async commands
|
||||||
|
- fully customizable keybindings
|
||||||
|
|
||||||
|
## Non-Features
|
||||||
|
|
||||||
|
- tabs or windows (handled by the window manager or the terminal multiplexer)
|
||||||
|
- built-in pager (handled by your pager of choice)
|
||||||
|
|
||||||
|
## May-Futures
|
||||||
|
|
||||||
|
- enchanced previews (image, pdf etc.)
|
||||||
|
- bookmarks
|
||||||
|
- colorschemes
|
||||||
|
- periodic refresh
|
||||||
|
- progress bar for file yank/delete paste
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
You can build from the source using:
|
||||||
|
|
||||||
|
go get -u github.com/gokcehan/lf
|
||||||
|
|
||||||
|
Currently there are no prebuilt binaries provided.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
After the installation `lf` command should start the application in the current directory.
|
||||||
|
|
||||||
|
See [tutorial](doc/tutorial.md) for an introduction to the configuration.
|
||||||
|
|
||||||
|
See [reference](doc/reference.md) for the list of keys, options and variables with their default values.
|
||||||
|
|
||||||
|
See [etc](etc) directory to integrate `lf` to your shell or editor.
|
||||||
|
|
||||||
|
## File Opener
|
||||||
|
|
||||||
|
lf does not come bundled with a file opener.
|
||||||
|
By default it tries to call `xdg-open` from `xdg-utils` package.
|
||||||
|
You can change the file opener using the opener option (e.g. `:set opener mimeopen`).
|
||||||
|
Below are a few alternatives you can use:
|
||||||
|
|
||||||
|
- [libfile-mimeinfo-perl](https://metacpan.org/release/File-MimeInfo) (executable name is `mimeopen`)
|
||||||
|
- [rifle](http://ranger.nongnu.org/) (ranger's default file opener)
|
||||||
|
- [mimeo](http://xyne.archlinux.ca/projects/mimeo/)
|
||||||
|
- custom (using file extensions and/or mimetypes with `file` command)
|
123
app.go
Normal file
123
app.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
ui *UI
|
||||||
|
nav *Nav
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitKey() error {
|
||||||
|
// TODO: this should be done with termbox somehow
|
||||||
|
|
||||||
|
cmd := exec.Command(envShell, "-c", "echo; echo -n 'Press any key to continue'; stty -echo; read -n 1; stty echo; echo")
|
||||||
|
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("waiting key: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) handleInp() {
|
||||||
|
for {
|
||||||
|
if gExitFlag {
|
||||||
|
log.Print("bye!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e := app.ui.getExpr()
|
||||||
|
if e == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e.eval(app, nil)
|
||||||
|
app.ui.draw(app.nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) exportVars() {
|
||||||
|
dir := app.nav.currDir()
|
||||||
|
|
||||||
|
var envFile string
|
||||||
|
if len(dir.fi) != 0 {
|
||||||
|
envFile = app.nav.currPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
marks := app.nav.currMarks()
|
||||||
|
|
||||||
|
envFiles := strings.Join(marks, ":")
|
||||||
|
|
||||||
|
os.Setenv("f", envFile)
|
||||||
|
os.Setenv("fs", envFiles)
|
||||||
|
|
||||||
|
if len(marks) == 0 {
|
||||||
|
os.Setenv("fx", envFile)
|
||||||
|
} else {
|
||||||
|
os.Setenv("fx", envFiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function is used to run a command in shell. Following modes are used:
|
||||||
|
//
|
||||||
|
// Prefix Wait Async Stdin/Stdout/Stderr UI action (before/after)
|
||||||
|
// $ No No Yes Do nothing and then sync
|
||||||
|
// ! Yes No Yes pause and then resume
|
||||||
|
// & No Yes No Do nothing
|
||||||
|
//
|
||||||
|
// Waiting async commands are not used for now.
|
||||||
|
func (app *App) runShell(s string, args []string, wait bool, async bool) {
|
||||||
|
app.exportVars()
|
||||||
|
|
||||||
|
if len(gOpts.ifs) != 0 {
|
||||||
|
s = fmt.Sprintf("IFS='%s'; %s", gOpts.ifs, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append([]string{"-c", s, "--"}, args...)
|
||||||
|
cmd := exec.Command(envShell, args...)
|
||||||
|
|
||||||
|
if !async {
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
if wait {
|
||||||
|
app.ui.pause()
|
||||||
|
defer app.ui.resume()
|
||||||
|
} else {
|
||||||
|
defer app.ui.sync()
|
||||||
|
}
|
||||||
|
|
||||||
|
defer app.nav.renew(app.ui.wins[0].h)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if async {
|
||||||
|
err = cmd.Start()
|
||||||
|
} else {
|
||||||
|
err = cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("running shell: %s", err)
|
||||||
|
app.ui.message = msg
|
||||||
|
log.Print(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if wait {
|
||||||
|
if err := waitKey(); err != nil {
|
||||||
|
msg := fmt.Sprintf("waiting shell: %s", err)
|
||||||
|
app.ui.message = msg
|
||||||
|
log.Print(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
112
client.go
Normal file
112
client.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/nsf/termbox-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func client() {
|
||||||
|
logFile, err := os.Create(gLogPath)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer logFile.Close()
|
||||||
|
log.SetOutput(logFile)
|
||||||
|
|
||||||
|
log.Print("hi!")
|
||||||
|
|
||||||
|
if err := termbox.Init(); err != nil {
|
||||||
|
log.Fatalf("initializing termbox: %s", err)
|
||||||
|
}
|
||||||
|
defer termbox.Close()
|
||||||
|
|
||||||
|
ui := newUI()
|
||||||
|
nav := newNav(ui.wins[0].h)
|
||||||
|
app := &App{ui, nav}
|
||||||
|
|
||||||
|
rcFile, err := os.Open(gConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("opening configuration file: %s", err)
|
||||||
|
app.ui.message = msg
|
||||||
|
log.Printf(msg)
|
||||||
|
} else {
|
||||||
|
app.ui.echoFileInfo(app.nav)
|
||||||
|
}
|
||||||
|
defer rcFile.Close()
|
||||||
|
|
||||||
|
p := newParser(rcFile)
|
||||||
|
for p.parse() {
|
||||||
|
p.expr.eval(app, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: parser error check
|
||||||
|
|
||||||
|
app.ui.draw(app.nav)
|
||||||
|
|
||||||
|
app.handleInp()
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveFiles(list []string, keep bool) error {
|
||||||
|
c, err := net.Dial("unix", gSocketPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dialing to save files: %s", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
log.Printf("saving files: %v", list)
|
||||||
|
|
||||||
|
fmt.Fprintln(c, "save")
|
||||||
|
|
||||||
|
if keep {
|
||||||
|
fmt.Fprintln(c, "keep")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintln(c, "move")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range list {
|
||||||
|
fmt.Fprintln(c, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadFiles() (list []string, keep bool, err error) {
|
||||||
|
c, e := net.Dial("unix", gSocketPath)
|
||||||
|
if e != nil {
|
||||||
|
err = fmt.Errorf("dialing to load files: %s", e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
fmt.Fprintln(c, "load")
|
||||||
|
|
||||||
|
s := bufio.NewScanner(c)
|
||||||
|
|
||||||
|
switch s.Scan(); s.Text() {
|
||||||
|
case "keep":
|
||||||
|
keep = true
|
||||||
|
case "move":
|
||||||
|
keep = false
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unexpected option to keep file(s): %s", s.Text())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for s.Scan() {
|
||||||
|
list = append(list, s.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Err() != nil {
|
||||||
|
err = fmt.Errorf("scanning file list: %s", s.Err())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("loading files: %v", list)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
39
doc/reference.md
Normal file
39
doc/reference.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Reference
|
||||||
|
|
||||||
|
## Keys
|
||||||
|
|
||||||
|
down (default "j")
|
||||||
|
up (default "k")
|
||||||
|
updir (default "h")
|
||||||
|
open (default "l")
|
||||||
|
quit (default "q")
|
||||||
|
bot (default "G")
|
||||||
|
top (default "gg")
|
||||||
|
read (default ":")
|
||||||
|
read-shell (default "$")
|
||||||
|
read-shell-wait (default "!")
|
||||||
|
read-shell-async (default "&")
|
||||||
|
search (default "/")
|
||||||
|
search-back (default "?")
|
||||||
|
toggle (default "<space>")
|
||||||
|
yank (default "y")
|
||||||
|
delete (default "d")
|
||||||
|
paste (default "p")
|
||||||
|
redraw (default "<c-l>")
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
preview bool (default on)
|
||||||
|
hidden bool (default off)
|
||||||
|
tabstop int (default 8)
|
||||||
|
scrolloff int (default 0)
|
||||||
|
sortby string (default name)
|
||||||
|
showinfo string (default none)
|
||||||
|
opener string (default xdg-open)
|
||||||
|
ratios string (default 1:2:3)
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
$f current file
|
||||||
|
$fs marked file(s) (seperated with ':')
|
||||||
|
$fx current file or marked file(s) if any
|
90
doc/tutorial.md
Normal file
90
doc/tutorial.md
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
# Tutorial
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The configuration file should be located in `~/.config/lf/lfrc`.
|
||||||
|
A sample configuration file can be found [here](etc/lfrc.example).
|
||||||
|
|
||||||
|
## Prefixes
|
||||||
|
|
||||||
|
The following command prefixes are used by `lf`:
|
||||||
|
|
||||||
|
: read (default)
|
||||||
|
$ read-shell
|
||||||
|
! read-shell-wait
|
||||||
|
& read-shell-async
|
||||||
|
/ search
|
||||||
|
? search-back
|
||||||
|
|
||||||
|
The same evaluator is used for the command line and the configuration file.
|
||||||
|
The difference is that prefixes are not necessary in the command line.
|
||||||
|
Instead different modes are provided to read corresponding commands.
|
||||||
|
Note that by default these modes are mapped to the prefix keys above.
|
||||||
|
|
||||||
|
## Syntax
|
||||||
|
|
||||||
|
Characters from `#` to `\n` are comments and ignored.
|
||||||
|
|
||||||
|
There are three special commands for configuration.
|
||||||
|
|
||||||
|
`set` is used to set an option which could be:
|
||||||
|
|
||||||
|
- bool (e.g. `set hidden`, `set nohidden`, `set hidden!`)
|
||||||
|
- int (e.g. `set scrolloff 10`)
|
||||||
|
- string (e.g. `set sortby time`)
|
||||||
|
|
||||||
|
`map` is used to bind a key to a command which could be:
|
||||||
|
|
||||||
|
- built-in command (e.g. `map gh cd ~`)
|
||||||
|
- custom command (e.g. `map dD trash`)
|
||||||
|
- shell command (e.g. `map i $less "$f"`, `map u !du -h . | less`)
|
||||||
|
|
||||||
|
`cmd` is used to define a custom command.
|
||||||
|
|
||||||
|
If there is no prefix then `:` is assumed.
|
||||||
|
An explicit `:` could be provided to group statements until a `\n` occurs.
|
||||||
|
This is especially useful for `map` and `cmd` commands.
|
||||||
|
If you need multiline you can wrap statements in `{{` and `}}` after the proper prefix.
|
||||||
|
|
||||||
|
## Custom Commands
|
||||||
|
|
||||||
|
To wrap up let us write a shell command to move selected file(s) to trash.
|
||||||
|
|
||||||
|
A first attempt to write such a command may look like this:
|
||||||
|
|
||||||
|
cmd trash ${{
|
||||||
|
mkdir -p ~/.trash
|
||||||
|
if [ -z $fs ]; then
|
||||||
|
mv --backup=numbered "$f" $HOME/.trash
|
||||||
|
else
|
||||||
|
IFS=':'; mv --backup=numbered $fs $HOME/.trash
|
||||||
|
fi
|
||||||
|
}}
|
||||||
|
|
||||||
|
We check `$fs` to see if there are any marked files.
|
||||||
|
Otherwise we just delete the current file.
|
||||||
|
Since this is such a common pattern, a separate `$fx` variable is provided.
|
||||||
|
We can use this variable to get rid of the conditional.
|
||||||
|
|
||||||
|
cmd trash ${{
|
||||||
|
mkdir -p ~/.trash
|
||||||
|
IFS=':'; mv --backup=numbered $fx $HOME/.trash
|
||||||
|
}}
|
||||||
|
|
||||||
|
The trash directory is checked each time the command is executed.
|
||||||
|
We can move it outside of the command so it would only run once at startup.
|
||||||
|
|
||||||
|
${{ mkdir -p ~/.trash }}
|
||||||
|
|
||||||
|
cmd trash ${{ IFS=':'; mv --backup=numbered $fx $HOME/.trash }}
|
||||||
|
|
||||||
|
Since these are one liners, we can drop `{{` and `}}.
|
||||||
|
|
||||||
|
$mkdir -p ~/.trash
|
||||||
|
|
||||||
|
cmd trash $IFS=':'; mv --backup=numbered $fx $HOME/.trash
|
||||||
|
|
||||||
|
Finally note that we set `IFS` variable accordingly in the command.
|
||||||
|
Instead we could use the `ifs` option to set it for all commands (e.g. `set ifs :`).
|
||||||
|
This could be especially useful for interactive use (e.g. `rm $fs` would simply work).
|
||||||
|
This option is not set by default as things may behave unexpectedly at other places.
|
21
etc/lf.sh
Normal file
21
etc/lf.sh
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# change the directory to last dir on exit
|
||||||
|
# adapted from the similar script for ranger
|
||||||
|
#
|
||||||
|
# you need to add something like the following to your shell rc file (e.g. ~/.bashrc):
|
||||||
|
#
|
||||||
|
# LFSH="$GOPATH/src/github.com/gokcehan/lf/etc/lf.sh"
|
||||||
|
# if [ -f "$LFSH" ]; then
|
||||||
|
# source "$LFSH"
|
||||||
|
# bind '"\C-o":"\C-ulf\C-m"'
|
||||||
|
# fi
|
||||||
|
#
|
||||||
|
|
||||||
|
lf () {
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
command lf -last-dir-path="$tmp" "$@"
|
||||||
|
if [ -f "$tmp" ]; then
|
||||||
|
dir="$(cat "$tmp")"
|
||||||
|
[ "$dir" != "$(pwd)" ] && cd "$dir"
|
||||||
|
fi
|
||||||
|
rm -f "$tmp"
|
||||||
|
}
|
31
etc/lf.vim
Normal file
31
etc/lf.vim
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
" use lf to select and open a file in vim
|
||||||
|
" adapted from the similar script for ranger
|
||||||
|
"
|
||||||
|
" you need to add something like the following to your ~/.vimrc:
|
||||||
|
"
|
||||||
|
" let lfvim = $GOPATH . "/src/github.com/gokcehan/lf/etc/lf.vim"
|
||||||
|
" if filereadable(lfvim)
|
||||||
|
" exec "source " . lfvim
|
||||||
|
" nnoremap <leader>l :<c-u>StartLF<cr>
|
||||||
|
" endif
|
||||||
|
"
|
||||||
|
|
||||||
|
function! StartLF()
|
||||||
|
let temp = tempname()
|
||||||
|
exec 'silent !lf -selection-path=' . shellescape(temp)
|
||||||
|
if !filereadable(temp)
|
||||||
|
redraw!
|
||||||
|
return
|
||||||
|
endif
|
||||||
|
let names = readfile(temp)
|
||||||
|
if empty(names)
|
||||||
|
redraw!
|
||||||
|
return
|
||||||
|
endif
|
||||||
|
exec 'edit ' . fnameescape(names[0])
|
||||||
|
for name in names[1:]
|
||||||
|
exec 'argadd ' . fnameescape(name)
|
||||||
|
endfor
|
||||||
|
redraw!
|
||||||
|
endfunction
|
||||||
|
command! -bar StartLF call StartLF()
|
88
etc/lfrc.example
Normal file
88
etc/lfrc.example
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# mc alike mode (single column)
|
||||||
|
# you may also consider setting an alias for automatically splitting windows in tmux
|
||||||
|
# (e.g. "alias mc='tmux split -h lf; lf'")
|
||||||
|
#set ratios 1
|
||||||
|
#set nopreview
|
||||||
|
#set showinfo size
|
||||||
|
|
||||||
|
# leave some space at the top and the bottom of the screen
|
||||||
|
set scrolloff 10
|
||||||
|
|
||||||
|
# mappings for pager and editor (change as you like)
|
||||||
|
map i $less "$f"
|
||||||
|
map e $vim "$f"
|
||||||
|
|
||||||
|
# use enter for shell commands
|
||||||
|
map <cr> read-shell
|
||||||
|
|
||||||
|
# execute current file (must be executable)
|
||||||
|
map x $"$f"
|
||||||
|
map X !"$f"
|
||||||
|
|
||||||
|
# you can either set your opener to 'mimeopen'
|
||||||
|
#set opener mimeopen
|
||||||
|
# or set dedicated keys for specific actions
|
||||||
|
map o &mimeopen "$f"
|
||||||
|
map m !mimeopen --ask "$f"
|
||||||
|
|
||||||
|
# rename current file without overwrite
|
||||||
|
cmd rename $[ -e "$1" ] || mv "$f" "$1"
|
||||||
|
|
||||||
|
# show disk usage
|
||||||
|
cmd usage $du -h . | less
|
||||||
|
|
||||||
|
# make sure trash folder exists
|
||||||
|
$mkdir -p $HOME/.trash
|
||||||
|
|
||||||
|
# move current file or selected files to trash folder
|
||||||
|
# see 'man mv' or 'mv --help' for backup options
|
||||||
|
cmd trash $IFS=':'; mv --backup=numbered $fx $HOME/.trash
|
||||||
|
|
||||||
|
# here be dragons
|
||||||
|
#map dD trash
|
||||||
|
|
||||||
|
# common directories
|
||||||
|
map gh cd ~
|
||||||
|
map gr cd /
|
||||||
|
|
||||||
|
# easily toggle options
|
||||||
|
map zp set preview!
|
||||||
|
map zh set hidden!
|
||||||
|
|
||||||
|
# easily select what information to show
|
||||||
|
map zn set showinfo none
|
||||||
|
map zs set showinfo size
|
||||||
|
map zt set showinfo time
|
||||||
|
|
||||||
|
# sort files and show the corresponding info
|
||||||
|
map sn :set sortby name; set showinfo none;
|
||||||
|
map ss :set sortby size; set showinfo size;
|
||||||
|
map st :set sortby time; set showinfo time;
|
||||||
|
|
||||||
|
# sets internal field seperator (IFS) to ":"
|
||||||
|
# useful for interactive use to automatically split file names in $fs and $fx
|
||||||
|
# things may behave unexpectedly so use with caution
|
||||||
|
#set ifs :
|
||||||
|
|
||||||
|
# extract the current file with the right command
|
||||||
|
# xkcd link: https://xkcd.com/1168/
|
||||||
|
cmd extract ${{
|
||||||
|
case "$f" in
|
||||||
|
*.tar.bz|*.tar.bz2|*.tbz|*.tbz2) tar xjvf "$f";;
|
||||||
|
*.tar.gz|*.tgz) tar xzvf "$f";;
|
||||||
|
*.tar.xz|*.txz) tar xJvf "$f";;
|
||||||
|
*.zip) unzip "$f";;
|
||||||
|
*.rar) unrar x "$f";;
|
||||||
|
*.7z) 7z x "$f";;
|
||||||
|
esac
|
||||||
|
}}
|
||||||
|
|
||||||
|
# compress selected files with tar and gunzip
|
||||||
|
# takes the name without '.tar.gz' suffix as an argument
|
||||||
|
# (e.g. ":compress foo" creates "foo.tar.gz")
|
||||||
|
cmd compress ${{
|
||||||
|
mkdir "$1"
|
||||||
|
IFS=':'; cp $fs "$1"
|
||||||
|
tar czvf "$1.tar.gz" "$1"
|
||||||
|
rm -rf "$1"
|
||||||
|
}}
|
284
eval.go
Normal file
284
eval.go
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *SetExpr) eval(app *App, args []string) {
|
||||||
|
switch e.opt {
|
||||||
|
case "hidden":
|
||||||
|
gOpts.hidden = true
|
||||||
|
app.nav.renew(app.nav.height)
|
||||||
|
case "nohidden":
|
||||||
|
gOpts.hidden = false
|
||||||
|
app.nav.renew(app.nav.height)
|
||||||
|
case "hidden!":
|
||||||
|
gOpts.hidden = !gOpts.hidden
|
||||||
|
app.nav.renew(app.nav.height)
|
||||||
|
case "preview":
|
||||||
|
gOpts.preview = true
|
||||||
|
case "nopreview":
|
||||||
|
gOpts.preview = false
|
||||||
|
case "preview!":
|
||||||
|
gOpts.preview = !gOpts.preview
|
||||||
|
case "scrolloff":
|
||||||
|
n, err := strconv.Atoi(e.val)
|
||||||
|
if err != nil {
|
||||||
|
app.ui.message = err.Error()
|
||||||
|
log.Print(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if n < 0 {
|
||||||
|
msg := "scrolloff should be a non-negative number"
|
||||||
|
app.ui.message = msg
|
||||||
|
log.Print(msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
max := app.ui.wins[0].h/2 - 1
|
||||||
|
if n > max {
|
||||||
|
// TODO: stay at the same row while up/down in the middle
|
||||||
|
n = max
|
||||||
|
}
|
||||||
|
gOpts.scrolloff = n
|
||||||
|
case "tabstop":
|
||||||
|
n, err := strconv.Atoi(e.val)
|
||||||
|
if err != nil {
|
||||||
|
app.ui.message = err.Error()
|
||||||
|
log.Print(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if n <= 0 {
|
||||||
|
msg := "tabstop should be a positive number"
|
||||||
|
app.ui.message = msg
|
||||||
|
log.Print(msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gOpts.tabstop = n
|
||||||
|
case "ifs":
|
||||||
|
gOpts.ifs = e.val
|
||||||
|
case "showinfo":
|
||||||
|
if e.val != "none" && e.val != "size" && e.val != "time" {
|
||||||
|
msg := "showinfo should either be 'none', 'size' or 'time'"
|
||||||
|
app.ui.message = msg
|
||||||
|
log.Print(msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gOpts.showinfo = e.val
|
||||||
|
case "sortby":
|
||||||
|
if e.val != "name" && e.val != "size" && e.val != "time" {
|
||||||
|
msg := "sortby should either be 'name', 'size' or 'time'"
|
||||||
|
app.ui.message = msg
|
||||||
|
log.Print(msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gOpts.sortby = e.val
|
||||||
|
app.nav.renew(app.nav.height)
|
||||||
|
case "opener":
|
||||||
|
gOpts.opener = e.val
|
||||||
|
case "ratios":
|
||||||
|
toks := strings.Split(e.val, ":")
|
||||||
|
var rats []int
|
||||||
|
for _, s := range toks {
|
||||||
|
i, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rats = append(rats, i)
|
||||||
|
}
|
||||||
|
gOpts.ratios = rats
|
||||||
|
app.ui = newUI()
|
||||||
|
default:
|
||||||
|
msg := fmt.Sprintf("unknown option: %s", e.opt)
|
||||||
|
app.ui.message = msg
|
||||||
|
log.Print(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *MapExpr) eval(app *App, args []string) {
|
||||||
|
gOpts.keys[e.keys] = e.expr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *CmdExpr) eval(app *App, args []string) {
|
||||||
|
gOpts.cmds[e.name] = e.expr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *CallExpr) eval(app *App, args []string) {
|
||||||
|
// TODO: check for extra toks in each case
|
||||||
|
switch e.name {
|
||||||
|
case "quit":
|
||||||
|
gExitFlag = true
|
||||||
|
case "echo":
|
||||||
|
app.ui.message = strings.Join(e.args, " ")
|
||||||
|
case "down":
|
||||||
|
app.nav.down()
|
||||||
|
app.ui.echoFileInfo(app.nav)
|
||||||
|
case "up":
|
||||||
|
app.nav.up()
|
||||||
|
app.ui.echoFileInfo(app.nav)
|
||||||
|
case "updir":
|
||||||
|
err := app.nav.updir()
|
||||||
|
if err != nil {
|
||||||
|
app.ui.message = err.Error()
|
||||||
|
log.Print(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.ui.echoFileInfo(app.nav)
|
||||||
|
case "open":
|
||||||
|
dir := app.nav.currDir()
|
||||||
|
|
||||||
|
if len(dir.fi) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
curr := app.nav.currFile()
|
||||||
|
path := app.nav.currPath()
|
||||||
|
|
||||||
|
if !curr.IsDir() && gSelectionPath == "" {
|
||||||
|
if len(app.nav.marks) == 0 {
|
||||||
|
app.runShell(fmt.Sprintf("%s '%s'", gOpts.opener, path), nil, false, false)
|
||||||
|
} else {
|
||||||
|
s := gOpts.opener
|
||||||
|
for m := range app.nav.marks {
|
||||||
|
s += fmt.Sprintf(" '%s'", m)
|
||||||
|
}
|
||||||
|
app.runShell(s, nil, false, false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := app.nav.open()
|
||||||
|
if err != nil {
|
||||||
|
app.ui.message = err.Error()
|
||||||
|
log.Print(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.ui.echoFileInfo(app.nav)
|
||||||
|
}
|
||||||
|
case "bot":
|
||||||
|
app.nav.bot()
|
||||||
|
app.ui.echoFileInfo(app.nav)
|
||||||
|
case "top":
|
||||||
|
app.nav.top()
|
||||||
|
app.ui.echoFileInfo(app.nav)
|
||||||
|
case "cd":
|
||||||
|
err := app.nav.cd(e.args[0])
|
||||||
|
if err != nil {
|
||||||
|
app.ui.message = err.Error()
|
||||||
|
log.Print(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.ui.echoFileInfo(app.nav)
|
||||||
|
case "read":
|
||||||
|
s := app.ui.prompt(":")
|
||||||
|
if len(s) == 0 {
|
||||||
|
app.ui.echoFileInfo(app.nav)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("command: %s", s)
|
||||||
|
p := newParser(strings.NewReader(s))
|
||||||
|
for p.parse() {
|
||||||
|
p.expr.eval(app, nil)
|
||||||
|
}
|
||||||
|
if p.err != nil {
|
||||||
|
app.ui.message = p.err.Error()
|
||||||
|
log.Print(p.err)
|
||||||
|
}
|
||||||
|
case "read-shell":
|
||||||
|
s := app.ui.prompt("$")
|
||||||
|
log.Printf("shell: %s", s)
|
||||||
|
app.runShell(s, nil, false, false)
|
||||||
|
case "read-shell-wait":
|
||||||
|
s := app.ui.prompt("!")
|
||||||
|
log.Printf("shell-wait: %s", s)
|
||||||
|
app.runShell(s, nil, true, false)
|
||||||
|
case "read-shell-async":
|
||||||
|
s := app.ui.prompt("&")
|
||||||
|
log.Printf("shell-async: %s", s)
|
||||||
|
app.runShell(s, nil, false, true)
|
||||||
|
case "search":
|
||||||
|
s := app.ui.prompt("/")
|
||||||
|
log.Printf("search: %s", s)
|
||||||
|
app.ui.message = "sorry, search is not implemented yet!"
|
||||||
|
// TODO: implement
|
||||||
|
case "search-back":
|
||||||
|
s := app.ui.prompt("?")
|
||||||
|
log.Printf("search-back: %s", s)
|
||||||
|
app.ui.message = "sorry, search-back is not implemented yet!"
|
||||||
|
// TODO: implement
|
||||||
|
case "toggle":
|
||||||
|
app.nav.toggle()
|
||||||
|
case "yank":
|
||||||
|
err := app.nav.save(true)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("yank: %s", err)
|
||||||
|
app.ui.message = msg
|
||||||
|
log.Printf(msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.nav.marks = make(map[string]bool)
|
||||||
|
case "delete":
|
||||||
|
err := app.nav.save(false)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("delete: %s", err)
|
||||||
|
app.ui.message = msg
|
||||||
|
log.Printf(msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.nav.marks = make(map[string]bool)
|
||||||
|
case "paste":
|
||||||
|
err := app.nav.paste()
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("paste: %s", err)
|
||||||
|
app.ui.message = msg
|
||||||
|
log.Printf(msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.nav.renew(app.nav.height)
|
||||||
|
app.nav.save(false)
|
||||||
|
saveFiles(nil, false)
|
||||||
|
case "redraw":
|
||||||
|
app.ui.renew()
|
||||||
|
app.nav.renew(app.ui.wins[0].h)
|
||||||
|
default:
|
||||||
|
cmd, ok := gOpts.cmds[e.name]
|
||||||
|
if !ok {
|
||||||
|
msg := fmt.Sprintf("command not found: %s", e.name)
|
||||||
|
app.ui.message = msg
|
||||||
|
log.Print(msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cmd.eval(app, e.args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ExecExpr) eval(app *App, args []string) {
|
||||||
|
switch e.pref {
|
||||||
|
case "$":
|
||||||
|
log.Printf("shell: %s -- %s", e, args)
|
||||||
|
app.ui.clearMsg()
|
||||||
|
app.runShell(e.expr, args, false, false)
|
||||||
|
app.ui.echoFileInfo(app.nav)
|
||||||
|
case "!":
|
||||||
|
log.Printf("shell-wait: %s -- %s", e, args)
|
||||||
|
app.runShell(e.expr, args, true, false)
|
||||||
|
case "&":
|
||||||
|
log.Printf("shell-async: %s -- %s", e, args)
|
||||||
|
app.runShell(e.expr, args, false, true)
|
||||||
|
case "/":
|
||||||
|
log.Printf("search: %s -- %s", e, args)
|
||||||
|
// TODO: implement
|
||||||
|
case "?":
|
||||||
|
log.Printf("search-back: %s -- %s", e, args)
|
||||||
|
// TODO: implement
|
||||||
|
default:
|
||||||
|
log.Printf("unknown execution prefix: %q", e.pref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ListExpr) eval(app *App, args []string) {
|
||||||
|
for _, expr := range e.exprs {
|
||||||
|
expr.eval(app, nil)
|
||||||
|
}
|
||||||
|
}
|
99
main.go
Normal file
99
main.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
envUser = os.Getenv("USER")
|
||||||
|
envHome = os.Getenv("HOME")
|
||||||
|
envHost = os.Getenv("HOSTNAME")
|
||||||
|
envShell = os.Getenv("SHELL")
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
gExitFlag bool
|
||||||
|
gSelectionPath string
|
||||||
|
gSocketPath string
|
||||||
|
gLogPath string
|
||||||
|
gServerLogPath string
|
||||||
|
gConfigPath string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if envUser == "" {
|
||||||
|
log.Fatal("$USER not set")
|
||||||
|
}
|
||||||
|
if envHome == "" {
|
||||||
|
envHome = "/home/" + envUser
|
||||||
|
}
|
||||||
|
if envHost == "" {
|
||||||
|
host, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("$HOSTNAME not set")
|
||||||
|
}
|
||||||
|
envHost = host
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp := os.TempDir()
|
||||||
|
|
||||||
|
gSocketPath = path.Join(tmp, fmt.Sprintf("lf.%s.sock", envUser))
|
||||||
|
|
||||||
|
// TODO: unique log file for each client
|
||||||
|
gLogPath = path.Join(tmp, fmt.Sprintf("lf.%s.log", envUser))
|
||||||
|
gServerLogPath = path.Join(tmp, fmt.Sprintf("lf.%s.server.log", envUser))
|
||||||
|
|
||||||
|
// TODO: xdg-config-home etc.
|
||||||
|
gConfigPath = path.Join(envHome, ".config", "lf", "lfrc")
|
||||||
|
}
|
||||||
|
|
||||||
|
func startServer() {
|
||||||
|
cmd := exec.Command(os.Args[0], "-server")
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
serverMode := flag.Bool("server", false, "start server (automatic)")
|
||||||
|
lastDirPath := flag.String("last-dir-path", "", "path to the file to write the last dir on exit (to use for cd)")
|
||||||
|
flag.StringVar(&gSelectionPath, "selection-path", "", "path to the file to write selected files on exit (to use as open file dialog)")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *serverMode {
|
||||||
|
serve()
|
||||||
|
} else {
|
||||||
|
// TODO: check if the socket is working
|
||||||
|
_, err := os.Stat(gSocketPath)
|
||||||
|
if err != nil {
|
||||||
|
startServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
client()
|
||||||
|
}
|
||||||
|
|
||||||
|
if *lastDirPath != "" {
|
||||||
|
f, err := os.Create(*lastDirPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = f.WriteString(wd)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
111
misc.go
Normal file
111
misc.go
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isRoot(name string) bool {
|
||||||
|
return path.Dir(name) == name
|
||||||
|
}
|
||||||
|
|
||||||
|
func humanize(size int64) string {
|
||||||
|
if size < 1000 {
|
||||||
|
return fmt.Sprintf("%d", size)
|
||||||
|
}
|
||||||
|
|
||||||
|
suffix := []string{
|
||||||
|
"K", // kilo
|
||||||
|
"M", // mega
|
||||||
|
"G", // giga
|
||||||
|
"T", // tera
|
||||||
|
"P", // peta
|
||||||
|
"E", // exa
|
||||||
|
"Z", // zeta
|
||||||
|
"Y", // yotta
|
||||||
|
}
|
||||||
|
|
||||||
|
curr := float64(size) / 1000
|
||||||
|
for _, s := range suffix {
|
||||||
|
if curr < 10 {
|
||||||
|
return fmt.Sprintf("%.1f%s", curr, s)
|
||||||
|
} else if curr < 1000 {
|
||||||
|
return fmt.Sprintf("%d%s", int(curr), s)
|
||||||
|
}
|
||||||
|
curr /= 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function extracts numbers from a string and returns with the rest.
|
||||||
|
// It is used for numeric sorting of files when the file name consists of
|
||||||
|
// both digits and letters.
|
||||||
|
//
|
||||||
|
// For instance if your input is 'foo123bar456' you get a slice of number
|
||||||
|
// consisting of elements '123' and '456' and rest of the string as a slice
|
||||||
|
// consisting of elements 'foo' and 'bar'. The last return argument denotes
|
||||||
|
// whether or not the first partition is a number.
|
||||||
|
func extractNums(s string) (nums []int, rest []string, numFirst bool) {
|
||||||
|
var buf []rune
|
||||||
|
|
||||||
|
r, _ := utf8.DecodeRuneInString(s)
|
||||||
|
digit := unicode.IsDigit(r)
|
||||||
|
numFirst = digit
|
||||||
|
|
||||||
|
for i, c := range s {
|
||||||
|
if unicode.IsDigit(c) == digit {
|
||||||
|
buf = append(buf, c)
|
||||||
|
if i != len(s)-1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if digit {
|
||||||
|
i, err := strconv.Atoi(string(buf))
|
||||||
|
if err != nil {
|
||||||
|
// TODO: handle error
|
||||||
|
log.Printf("extracting numbers: %s", err)
|
||||||
|
}
|
||||||
|
nums = append(nums, i)
|
||||||
|
} else {
|
||||||
|
rest = append(rest, string(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = nil
|
||||||
|
buf = append(buf, c)
|
||||||
|
digit = !digit
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func max(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// We don't need no generic code
|
||||||
|
// We don't need no thought control
|
||||||
|
// No dark templates in compiler
|
||||||
|
// Haskell leave them kids alone
|
||||||
|
// Hey Bjarne leave them kids alone
|
||||||
|
// All in all it's just another brick in the code
|
||||||
|
// All in all you're just another brick in the code
|
||||||
|
//
|
||||||
|
// -- Pink Trolled --
|
||||||
|
//
|
450
nav.go
Normal file
450
nav.go
Normal file
@ -0,0 +1,450 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Dir struct {
|
||||||
|
ind int // which entry is highlighted
|
||||||
|
pos int // which line in the ui highlighted entry is
|
||||||
|
path string
|
||||||
|
fi []os.FileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type ByName []os.FileInfo
|
||||||
|
|
||||||
|
func (a ByName) Len() int { return len(a) }
|
||||||
|
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
|
||||||
|
func (a ByName) Less(i, j int) bool {
|
||||||
|
return strings.ToLower(a[i].Name()) < strings.ToLower(a[j].Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
type BySize []os.FileInfo
|
||||||
|
|
||||||
|
func (a BySize) Len() int { return len(a) }
|
||||||
|
func (a BySize) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a BySize) Less(i, j int) bool { return a[i].Size() < a[j].Size() }
|
||||||
|
|
||||||
|
type ByTime []os.FileInfo
|
||||||
|
|
||||||
|
func (a ByTime) Len() int { return len(a) }
|
||||||
|
func (a ByTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a ByTime) Less(i, j int) bool { return a[i].ModTime().Before(a[j].ModTime()) }
|
||||||
|
|
||||||
|
type ByDir []os.FileInfo
|
||||||
|
|
||||||
|
func (a ByDir) Len() int { return len(a) }
|
||||||
|
func (a ByDir) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
|
||||||
|
func (a ByDir) Less(i, j int) bool {
|
||||||
|
if a[i].IsDir() == a[j].IsDir() {
|
||||||
|
return i < j
|
||||||
|
}
|
||||||
|
return a[i].IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ByNum []os.FileInfo
|
||||||
|
|
||||||
|
func (a ByNum) Len() int { return len(a) }
|
||||||
|
func (a ByNum) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
|
||||||
|
func (a ByNum) Less(i, j int) bool {
|
||||||
|
nums1, rest1, numFirst1 := extractNums(a[i].Name())
|
||||||
|
nums2, rest2, numFirst2 := extractNums(a[j].Name())
|
||||||
|
|
||||||
|
if numFirst1 != numFirst2 {
|
||||||
|
return i < j
|
||||||
|
}
|
||||||
|
|
||||||
|
if numFirst1 {
|
||||||
|
if nums1[0] != nums2[0] {
|
||||||
|
return nums1[0] < nums2[0]
|
||||||
|
}
|
||||||
|
nums1 = nums1[1:]
|
||||||
|
nums2 = nums2[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := 0; k < len(nums1) && k < len(nums2); k++ {
|
||||||
|
if rest1[k] != rest2[k] {
|
||||||
|
return i < j
|
||||||
|
}
|
||||||
|
if nums1[k] != nums2[k] {
|
||||||
|
return nums1[k] < nums2[k]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return i < j
|
||||||
|
}
|
||||||
|
|
||||||
|
func organizeFiles(fi []os.FileInfo) []os.FileInfo {
|
||||||
|
if !gOpts.hidden {
|
||||||
|
var tmp []os.FileInfo
|
||||||
|
for _, f := range fi {
|
||||||
|
if f.Name()[0] != '.' {
|
||||||
|
tmp = append(tmp, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fi = tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
switch gOpts.sortby {
|
||||||
|
case "name":
|
||||||
|
sort.Sort(ByName(fi))
|
||||||
|
case "size":
|
||||||
|
sort.Sort(BySize(fi))
|
||||||
|
case "time":
|
||||||
|
sort.Sort(ByTime(fi))
|
||||||
|
default:
|
||||||
|
log.Printf("unknown sorting type: %s", gOpts.sortby)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: these should be optional
|
||||||
|
sort.Stable(ByNum(fi))
|
||||||
|
sort.Stable(ByDir(fi))
|
||||||
|
|
||||||
|
return fi
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDir(path string) *Dir {
|
||||||
|
fi, err := ioutil.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fi = organizeFiles(fi)
|
||||||
|
|
||||||
|
return &Dir{
|
||||||
|
path: path,
|
||||||
|
fi: fi,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dir *Dir) renew(height int) {
|
||||||
|
fi, err := ioutil.ReadDir(dir.path)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fi = organizeFiles(fi)
|
||||||
|
|
||||||
|
var name string
|
||||||
|
if len(dir.fi) != 0 {
|
||||||
|
name = dir.fi[dir.ind].Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
dir.fi = fi
|
||||||
|
|
||||||
|
dir.load(dir.ind, dir.pos, height, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dir *Dir) load(ind, pos, height int, name string) {
|
||||||
|
if len(dir.fi) == 0 {
|
||||||
|
dir.ind, dir.pos = 0, 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ind = max(0, min(ind, len(dir.fi)-1))
|
||||||
|
|
||||||
|
if dir.fi[ind].Name() != name {
|
||||||
|
for i, f := range dir.fi {
|
||||||
|
if f.Name() == name {
|
||||||
|
ind = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
edge := min(gOpts.scrolloff, len(dir.fi)-ind-1)
|
||||||
|
pos = min(ind, height-edge-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir.ind = ind
|
||||||
|
dir.pos = pos
|
||||||
|
}
|
||||||
|
|
||||||
|
type Nav struct {
|
||||||
|
dirs []*Dir
|
||||||
|
inds map[string]int
|
||||||
|
poss map[string]int
|
||||||
|
names map[string]string
|
||||||
|
marks map[string]bool
|
||||||
|
height int
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDirs(wd string, height int) []*Dir {
|
||||||
|
var dirs []*Dir
|
||||||
|
|
||||||
|
for curr, base := wd, ""; !isRoot(base); curr, base = path.Dir(curr), path.Base(curr) {
|
||||||
|
dir := newDir(curr)
|
||||||
|
for i, f := range dir.fi {
|
||||||
|
if f.Name() == base {
|
||||||
|
dir.ind = i
|
||||||
|
edge := min(gOpts.scrolloff, len(dir.fi)-dir.ind-1)
|
||||||
|
dir.pos = min(i, height-edge-1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dirs = append(dirs, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, j := 0, len(dirs)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
dirs[i], dirs[j] = dirs[j], dirs[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return dirs
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNav(height int) *Nav {
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dirs := getDirs(wd, height)
|
||||||
|
|
||||||
|
return &Nav{
|
||||||
|
dirs: dirs,
|
||||||
|
inds: make(map[string]int),
|
||||||
|
poss: make(map[string]int),
|
||||||
|
names: make(map[string]string),
|
||||||
|
marks: make(map[string]bool),
|
||||||
|
height: height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nav *Nav) renew(height int) {
|
||||||
|
nav.height = height
|
||||||
|
for _, d := range nav.dirs {
|
||||||
|
d.renew(nav.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
for m := range nav.marks {
|
||||||
|
if _, err := os.Stat(m); os.IsNotExist(err) {
|
||||||
|
delete(nav.marks, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nav *Nav) down() {
|
||||||
|
dir := nav.currDir()
|
||||||
|
|
||||||
|
maxind := len(dir.fi) - 1
|
||||||
|
|
||||||
|
if dir.ind >= maxind {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dir.ind++
|
||||||
|
|
||||||
|
dir.pos++
|
||||||
|
edge := min(gOpts.scrolloff, maxind-dir.ind)
|
||||||
|
dir.pos = min(dir.pos, nav.height-edge-1)
|
||||||
|
dir.pos = min(dir.pos, maxind)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nav *Nav) up() {
|
||||||
|
dir := nav.currDir()
|
||||||
|
|
||||||
|
if dir.ind == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dir.ind--
|
||||||
|
|
||||||
|
dir.pos--
|
||||||
|
edge := min(gOpts.scrolloff, dir.ind)
|
||||||
|
dir.pos = max(dir.pos, edge)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nav *Nav) updir() error {
|
||||||
|
if len(nav.dirs) <= 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := nav.currDir()
|
||||||
|
|
||||||
|
nav.inds[dir.path] = dir.ind
|
||||||
|
nav.poss[dir.path] = dir.pos
|
||||||
|
|
||||||
|
if len(dir.fi) != 0 {
|
||||||
|
nav.names[dir.path] = dir.fi[dir.ind].Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.dirs = nav.dirs[:len(nav.dirs)-1]
|
||||||
|
|
||||||
|
err := os.Chdir(path.Dir(dir.path))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("updir: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nav *Nav) open() error {
|
||||||
|
curr := nav.currFile()
|
||||||
|
path := nav.currPath()
|
||||||
|
|
||||||
|
if curr.IsDir() {
|
||||||
|
dir := newDir(path)
|
||||||
|
|
||||||
|
dir.load(nav.inds[path], nav.poss[path], nav.height, nav.names[path])
|
||||||
|
|
||||||
|
nav.dirs = append(nav.dirs, dir)
|
||||||
|
|
||||||
|
err := os.Chdir(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open: %s", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
f, err := os.Create(gSelectionPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open: %s", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if len(nav.marks) != 0 {
|
||||||
|
marks := nav.currMarks()
|
||||||
|
path = strings.Join(marks, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = f.WriteString(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gExitFlag = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nav *Nav) bot() {
|
||||||
|
dir := nav.currDir()
|
||||||
|
|
||||||
|
dir.ind = len(dir.fi) - 1
|
||||||
|
dir.pos = min(dir.ind, nav.height-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nav *Nav) top() {
|
||||||
|
dir := nav.currDir()
|
||||||
|
|
||||||
|
dir.ind = 0
|
||||||
|
dir.pos = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nav *Nav) cd(wd string) error {
|
||||||
|
if !path.IsAbs(wd) {
|
||||||
|
wd = path.Join(nav.currDir().path, wd)
|
||||||
|
}
|
||||||
|
|
||||||
|
wd = strings.Replace(wd, "~", envHome, -1)
|
||||||
|
|
||||||
|
err := os.Chdir(wd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cd: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.dirs = getDirs(wd, nav.height)
|
||||||
|
|
||||||
|
// TODO: save/load ind and pos from the map
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nav *Nav) toggle() {
|
||||||
|
path := nav.currPath()
|
||||||
|
|
||||||
|
if nav.marks[path] {
|
||||||
|
delete(nav.marks, path)
|
||||||
|
} else {
|
||||||
|
nav.marks[path] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.down()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nav *Nav) save(keep bool) error {
|
||||||
|
if len(nav.marks) == 0 {
|
||||||
|
path := nav.currPath()
|
||||||
|
|
||||||
|
err := saveFiles([]string{path}, keep)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var fs []string
|
||||||
|
for f := range nav.marks {
|
||||||
|
fs = append(fs, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := saveFiles(fs, keep)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nav *Nav) paste() error {
|
||||||
|
list, keep, err := loadFiles()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(list) == 0 {
|
||||||
|
return errors.New("no file in yank/delete buffer")
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := nav.currDir()
|
||||||
|
|
||||||
|
args := append(list, dir.path)
|
||||||
|
|
||||||
|
var sh string
|
||||||
|
if keep {
|
||||||
|
sh = "cp"
|
||||||
|
} else {
|
||||||
|
sh = "mv"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(sh, args...)
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("%s: %s", sh, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: async?
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nav *Nav) currDir() *Dir {
|
||||||
|
return nav.dirs[len(nav.dirs)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nav *Nav) currFile() os.FileInfo {
|
||||||
|
last := nav.dirs[len(nav.dirs)-1]
|
||||||
|
return last.fi[last.ind]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nav *Nav) currPath() string {
|
||||||
|
last := nav.dirs[len(nav.dirs)-1]
|
||||||
|
curr := last.fi[last.ind]
|
||||||
|
return path.Join(last.path, curr.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nav *Nav) currMarks() []string {
|
||||||
|
marks := make([]string, 0, len(nav.marks))
|
||||||
|
for m := range nav.marks {
|
||||||
|
marks = append(marks, m)
|
||||||
|
}
|
||||||
|
return marks
|
||||||
|
}
|
52
opts.go
Normal file
52
opts.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
type Opts struct {
|
||||||
|
hidden bool
|
||||||
|
preview bool
|
||||||
|
scrolloff int
|
||||||
|
tabstop int
|
||||||
|
ifs string
|
||||||
|
showinfo string
|
||||||
|
sortby string
|
||||||
|
opener string
|
||||||
|
ratios []int
|
||||||
|
keys map[string]Expr
|
||||||
|
cmds map[string]Expr
|
||||||
|
}
|
||||||
|
|
||||||
|
var gOpts Opts
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gOpts.hidden = false
|
||||||
|
gOpts.preview = true
|
||||||
|
gOpts.scrolloff = 0
|
||||||
|
gOpts.tabstop = 8
|
||||||
|
gOpts.ifs = ""
|
||||||
|
gOpts.showinfo = "none"
|
||||||
|
gOpts.sortby = "name"
|
||||||
|
gOpts.opener = "xdg-open"
|
||||||
|
gOpts.ratios = []int{1, 2, 3}
|
||||||
|
|
||||||
|
gOpts.keys = make(map[string]Expr)
|
||||||
|
|
||||||
|
gOpts.keys["j"] = &CallExpr{"down", nil}
|
||||||
|
gOpts.keys["k"] = &CallExpr{"up", nil}
|
||||||
|
gOpts.keys["h"] = &CallExpr{"updir", nil}
|
||||||
|
gOpts.keys["l"] = &CallExpr{"open", nil}
|
||||||
|
gOpts.keys["q"] = &CallExpr{"quit", nil}
|
||||||
|
gOpts.keys["G"] = &CallExpr{"bot", nil}
|
||||||
|
gOpts.keys["gg"] = &CallExpr{"top", nil}
|
||||||
|
gOpts.keys[":"] = &CallExpr{"read", nil}
|
||||||
|
gOpts.keys["$"] = &CallExpr{"read-shell", nil}
|
||||||
|
gOpts.keys["!"] = &CallExpr{"read-shell-wait", nil}
|
||||||
|
gOpts.keys["&"] = &CallExpr{"read-shell-async", nil}
|
||||||
|
gOpts.keys["/"] = &CallExpr{"search", nil}
|
||||||
|
gOpts.keys["?"] = &CallExpr{"search-back", nil}
|
||||||
|
gOpts.keys["<space>"] = &CallExpr{"toggle", nil}
|
||||||
|
gOpts.keys["y"] = &CallExpr{"yank", nil}
|
||||||
|
gOpts.keys["d"] = &CallExpr{"delete", nil}
|
||||||
|
gOpts.keys["p"] = &CallExpr{"paste", nil}
|
||||||
|
gOpts.keys["<c-l>"] = &CallExpr{"redraw", nil}
|
||||||
|
|
||||||
|
gOpts.cmds = make(map[string]Expr)
|
||||||
|
}
|
243
parse.go
Normal file
243
parse.go
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// Grammar of the language used in the evaluator
|
||||||
|
//
|
||||||
|
// Expr = SetExpr
|
||||||
|
// | MapExpr
|
||||||
|
// | CmdExpr
|
||||||
|
// | CallExpr
|
||||||
|
// | ExecExpr
|
||||||
|
// | ListExpr
|
||||||
|
//
|
||||||
|
// SetExpr = 'set' <opt> <val> ';'
|
||||||
|
//
|
||||||
|
// MapExpr = 'map' <keys> Expr ';'
|
||||||
|
//
|
||||||
|
// CmdExpr = 'cmd' <name> Expr ';'
|
||||||
|
//
|
||||||
|
// CallExpr = <name> <args> ';'
|
||||||
|
//
|
||||||
|
// ExecExpr = Prefix <expr> '\n'
|
||||||
|
// | Prefix '{{' <expr> '}}' ';'
|
||||||
|
//
|
||||||
|
// Prefix = '$' | '!' | '&' | '/' | '?'
|
||||||
|
//
|
||||||
|
// ListExpr = ':' ListExpr '\n'
|
||||||
|
// | ':' '{{' ListRest '}}' ';'
|
||||||
|
//
|
||||||
|
// ListRest = Nil
|
||||||
|
// | Expr ListRest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Expr interface {
|
||||||
|
String() string
|
||||||
|
|
||||||
|
eval(app *App, args []string)
|
||||||
|
// TODO: add a bind method to avoid passing args in eval
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetExpr struct {
|
||||||
|
opt string
|
||||||
|
val string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SetExpr) String() string { return fmt.Sprintf("set %s %s", e.opt, e.val) }
|
||||||
|
|
||||||
|
type MapExpr struct {
|
||||||
|
keys string
|
||||||
|
expr Expr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *MapExpr) String() string { return fmt.Sprintf("map %s %s", e.keys, e.expr) }
|
||||||
|
|
||||||
|
type CmdExpr struct {
|
||||||
|
name string
|
||||||
|
expr Expr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *CmdExpr) String() string { return fmt.Sprintf("cmd %s %s", e.name, e.expr) }
|
||||||
|
|
||||||
|
type CallExpr struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *CallExpr) String() string { return fmt.Sprintf("%s -- %s", e.name, e.args) }
|
||||||
|
|
||||||
|
type ExecExpr struct {
|
||||||
|
pref string
|
||||||
|
expr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ExecExpr) String() string { return fmt.Sprintf("%s %s", e.pref, e.expr) }
|
||||||
|
|
||||||
|
type ListExpr struct {
|
||||||
|
exprs []Expr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ListExpr) String() string {
|
||||||
|
buf := []byte{':', '{', '{', ' '}
|
||||||
|
for _, expr := range e.exprs {
|
||||||
|
buf = append(buf, expr.String()...)
|
||||||
|
buf = append(buf, ';', ' ')
|
||||||
|
}
|
||||||
|
buf = append(buf, '}', '}')
|
||||||
|
return string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Parser struct {
|
||||||
|
scanner *Scanner
|
||||||
|
expr Expr
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func newParser(r io.Reader) *Parser {
|
||||||
|
scanner := newScanner(r)
|
||||||
|
|
||||||
|
scanner.scan()
|
||||||
|
|
||||||
|
return &Parser{
|
||||||
|
scanner: scanner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) parseExpr() Expr {
|
||||||
|
s := p.scanner
|
||||||
|
|
||||||
|
var result Expr
|
||||||
|
|
||||||
|
// TODO: syntax error check
|
||||||
|
|
||||||
|
switch s.typ {
|
||||||
|
case TokenEOF:
|
||||||
|
return nil
|
||||||
|
case TokenSemicolon:
|
||||||
|
s.scan()
|
||||||
|
case TokenIdent:
|
||||||
|
switch s.tok {
|
||||||
|
case "set":
|
||||||
|
s.scan()
|
||||||
|
opt := s.tok
|
||||||
|
|
||||||
|
s.scan()
|
||||||
|
var val string
|
||||||
|
if s.typ != TokenSemicolon {
|
||||||
|
val = s.tok
|
||||||
|
s.scan()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.scan()
|
||||||
|
|
||||||
|
result = &SetExpr{opt, val}
|
||||||
|
case "map":
|
||||||
|
s.scan()
|
||||||
|
keys := s.tok
|
||||||
|
|
||||||
|
s.scan()
|
||||||
|
expr := p.parseExpr()
|
||||||
|
|
||||||
|
result = &MapExpr{keys, expr}
|
||||||
|
case "cmd":
|
||||||
|
s.scan()
|
||||||
|
name := s.tok
|
||||||
|
|
||||||
|
s.scan()
|
||||||
|
expr := p.parseExpr()
|
||||||
|
|
||||||
|
result = &CmdExpr{name, expr}
|
||||||
|
default:
|
||||||
|
name := s.tok
|
||||||
|
|
||||||
|
var args []string
|
||||||
|
for s.scan() && s.typ != TokenSemicolon {
|
||||||
|
args = append(args, s.tok)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.scan()
|
||||||
|
|
||||||
|
if s.err != nil {
|
||||||
|
p.err = fmt.Errorf("parsing: %s", s.err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result = &CallExpr{name, args}
|
||||||
|
}
|
||||||
|
case TokenColon:
|
||||||
|
s.scan()
|
||||||
|
|
||||||
|
var exprs []Expr
|
||||||
|
if s.typ == TokenLBraces {
|
||||||
|
s.scan()
|
||||||
|
for {
|
||||||
|
e := p.parseExpr()
|
||||||
|
if e == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
exprs = append(exprs, e)
|
||||||
|
if s.typ == TokenRBraces {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.scan()
|
||||||
|
} else {
|
||||||
|
for {
|
||||||
|
e := p.parseExpr()
|
||||||
|
if e == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
exprs = append(exprs, e)
|
||||||
|
if s.tok == "\n" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.scan()
|
||||||
|
|
||||||
|
result = &ListExpr{exprs}
|
||||||
|
case TokenPrefix:
|
||||||
|
pref := s.tok
|
||||||
|
|
||||||
|
s.scan()
|
||||||
|
|
||||||
|
var expr string
|
||||||
|
if s.typ == TokenLBraces {
|
||||||
|
s.scan()
|
||||||
|
expr = s.tok
|
||||||
|
s.scan()
|
||||||
|
} else if s.typ == TokenCommand {
|
||||||
|
expr = s.tok
|
||||||
|
} else {
|
||||||
|
// TODO: handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
s.scan()
|
||||||
|
s.scan()
|
||||||
|
|
||||||
|
if s.err != nil {
|
||||||
|
p.err = fmt.Errorf("parsing: %s", s.err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result = &ExecExpr{pref, expr}
|
||||||
|
default:
|
||||||
|
// TODO: handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("parsed:", result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) parse() bool {
|
||||||
|
if p.expr = p.parseExpr(); p.expr == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
244
scan.go
Normal file
244
scan.go
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TokenErr TokenType = iota
|
||||||
|
TokenEOF // end of file
|
||||||
|
// no explicit keyword type
|
||||||
|
TokenIdent // e.g. set, ratios, 1:2:3
|
||||||
|
TokenColon // :
|
||||||
|
TokenPrefix // $, !, &, / or ?
|
||||||
|
TokenLBraces // {{
|
||||||
|
TokenRBraces // }}
|
||||||
|
TokenCommand // in between a prefix to \n or between {{ and }}
|
||||||
|
TokenSemicolon // ;
|
||||||
|
// comments are stripped
|
||||||
|
)
|
||||||
|
|
||||||
|
type Scanner struct {
|
||||||
|
buf []byte // input buffer
|
||||||
|
off int // current offset in buf
|
||||||
|
chr byte // current character in buf
|
||||||
|
sem bool // insert semicolon
|
||||||
|
nln bool // insert newline
|
||||||
|
eof bool // buffer ended
|
||||||
|
blk bool // scanning block
|
||||||
|
cmd bool // scanning command
|
||||||
|
typ TokenType // scanned token type
|
||||||
|
tok string // scanned token value
|
||||||
|
err error // error if any
|
||||||
|
// TODO: pos
|
||||||
|
}
|
||||||
|
|
||||||
|
func newScanner(r io.Reader) *Scanner {
|
||||||
|
// TODO: read input beforehand
|
||||||
|
buf, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("scanning: %s", err)
|
||||||
|
// app.ui.message = msg
|
||||||
|
log.Print(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
var eof bool
|
||||||
|
var chr byte
|
||||||
|
|
||||||
|
if len(buf) == 0 {
|
||||||
|
eof = true
|
||||||
|
} else {
|
||||||
|
eof = false
|
||||||
|
chr = buf[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Scanner{
|
||||||
|
buf: buf,
|
||||||
|
eof: eof,
|
||||||
|
chr: chr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) next() {
|
||||||
|
if s.off+1 < len(s.buf) {
|
||||||
|
// TODO: unicode
|
||||||
|
s.off++
|
||||||
|
s.chr = s.buf[s.off]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.off = len(s.buf)
|
||||||
|
s.chr = 0
|
||||||
|
s.eof = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) peek() byte {
|
||||||
|
if s.off+1 < len(s.buf) {
|
||||||
|
// TODO: unicode
|
||||||
|
return s.buf[s.off+1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSpace(b byte) bool {
|
||||||
|
return unicode.IsSpace(rune(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPrefix(b byte) bool {
|
||||||
|
// TODO: how to differentiate slash in path vs search?
|
||||||
|
return b == '$' || b == '!' || b == '&' // || b == '/' || b == '?'
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) scan() bool {
|
||||||
|
// log.Println("scanning:", s.tok)
|
||||||
|
scan:
|
||||||
|
switch {
|
||||||
|
case s.eof:
|
||||||
|
s.next()
|
||||||
|
if s.sem {
|
||||||
|
s.typ = TokenSemicolon
|
||||||
|
s.tok = "\n"
|
||||||
|
s.sem = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if s.nln {
|
||||||
|
s.typ = TokenSemicolon
|
||||||
|
s.tok = "\n"
|
||||||
|
s.nln = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
case s.blk:
|
||||||
|
// return here by setting s.cmd to false
|
||||||
|
// after scanning the command in the loop below
|
||||||
|
if !s.cmd {
|
||||||
|
s.next()
|
||||||
|
s.next()
|
||||||
|
s.typ = TokenRBraces
|
||||||
|
s.tok = "}}"
|
||||||
|
s.blk = false
|
||||||
|
s.sem = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
beg := s.off
|
||||||
|
|
||||||
|
for !s.eof {
|
||||||
|
s.next()
|
||||||
|
if s.chr == '}' {
|
||||||
|
if !s.eof && s.peek() == '}' {
|
||||||
|
s.typ = TokenCommand
|
||||||
|
s.tok = string(s.buf[beg:s.off])
|
||||||
|
s.cmd = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: fill error
|
||||||
|
return false
|
||||||
|
case s.cmd:
|
||||||
|
for !s.eof && isSpace(s.chr) {
|
||||||
|
s.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.eof && s.chr == '{' {
|
||||||
|
if s.peek() == '{' {
|
||||||
|
s.next()
|
||||||
|
s.next()
|
||||||
|
s.typ = TokenLBraces
|
||||||
|
s.tok = "{{"
|
||||||
|
s.blk = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beg := s.off
|
||||||
|
|
||||||
|
for !s.eof && s.chr != '\n' {
|
||||||
|
s.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.typ = TokenCommand
|
||||||
|
s.tok = string(s.buf[beg:s.off])
|
||||||
|
s.cmd = false
|
||||||
|
s.sem = true
|
||||||
|
case s.chr == '\n':
|
||||||
|
s.next()
|
||||||
|
if s.sem {
|
||||||
|
s.typ = TokenSemicolon
|
||||||
|
s.tok = "\n"
|
||||||
|
s.sem = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if s.nln {
|
||||||
|
s.typ = TokenSemicolon
|
||||||
|
s.tok = "\n"
|
||||||
|
s.nln = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
goto scan
|
||||||
|
case isSpace(s.chr):
|
||||||
|
for !s.eof && isSpace(s.chr) {
|
||||||
|
s.next()
|
||||||
|
}
|
||||||
|
goto scan
|
||||||
|
case s.chr == ';':
|
||||||
|
s.typ = TokenSemicolon
|
||||||
|
s.tok = ";"
|
||||||
|
s.sem = false
|
||||||
|
s.next()
|
||||||
|
case s.chr == '#':
|
||||||
|
for !s.eof && s.chr != '\n' {
|
||||||
|
s.next()
|
||||||
|
}
|
||||||
|
goto scan
|
||||||
|
case s.chr == ':':
|
||||||
|
s.typ = TokenColon
|
||||||
|
s.tok = ":"
|
||||||
|
s.nln = true
|
||||||
|
s.next()
|
||||||
|
case s.chr == '{':
|
||||||
|
if s.peek() == '{' {
|
||||||
|
s.next()
|
||||||
|
s.next()
|
||||||
|
s.typ = TokenLBraces
|
||||||
|
s.tok = "{{"
|
||||||
|
s.sem = false
|
||||||
|
s.nln = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// TODO: handle error
|
||||||
|
case s.chr == '}':
|
||||||
|
if s.peek() == '}' {
|
||||||
|
s.next()
|
||||||
|
s.next()
|
||||||
|
s.typ = TokenRBraces
|
||||||
|
s.tok = "}}"
|
||||||
|
s.sem = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// TODO: handle error
|
||||||
|
case isPrefix(s.chr):
|
||||||
|
s.typ = TokenPrefix
|
||||||
|
s.tok = string(s.chr)
|
||||||
|
s.cmd = true
|
||||||
|
s.next()
|
||||||
|
default:
|
||||||
|
beg := s.off
|
||||||
|
for !s.eof && !isSpace(s.chr) && s.chr != ';' && s.chr != '#' {
|
||||||
|
s.next()
|
||||||
|
}
|
||||||
|
s.typ = TokenIdent
|
||||||
|
s.tok = string(s.buf[beg:s.off])
|
||||||
|
s.sem = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
125
scan_test.go
Normal file
125
scan_test.go
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: use a slice and merge with outputs
|
||||||
|
|
||||||
|
var inp0 = ""
|
||||||
|
var inp1 = "# comments start with '#'"
|
||||||
|
var inp2 = "set hidden # trailing comments are allowed"
|
||||||
|
var inp3 = "set hidden; set preview"
|
||||||
|
var inp4 = "set ratios 1:2:3"
|
||||||
|
var inp5 = "set ratios 1:2:3;"
|
||||||
|
var inp6 = ":set ratios 1:2:3"
|
||||||
|
var inp7 = ":set ratios 1:2:3;"
|
||||||
|
var inp8 = "map gh cd ~"
|
||||||
|
var inp9 = "map gh cd ~;"
|
||||||
|
var inp10 = "map gh :cd ~"
|
||||||
|
var inp11 = "map gh :cd ~;"
|
||||||
|
var inp12 = "cmd usage $du -h . | less"
|
||||||
|
var inp13 = "map u usage"
|
||||||
|
var inp14 = "map u usage;"
|
||||||
|
var inp15 = "map u :usage"
|
||||||
|
var inp16 = "map u :usage;"
|
||||||
|
var inp17 = "map u $du -h . | less"
|
||||||
|
var inp18 = `cmd usage $du -h "$1" | less`
|
||||||
|
var inp19 = "map u usage /"
|
||||||
|
|
||||||
|
var inp20 = `cmd gohome :{{
|
||||||
|
cd ~
|
||||||
|
set hidden
|
||||||
|
}}`
|
||||||
|
|
||||||
|
var inp21 = `map gh :{{
|
||||||
|
cd ~
|
||||||
|
set hidden
|
||||||
|
}}`
|
||||||
|
|
||||||
|
var inp22 = `map c ${{
|
||||||
|
mkdir foo
|
||||||
|
IFS=':'; cp ${fs} foo
|
||||||
|
tar -czvf "foo.tar.gz" foo
|
||||||
|
rm -rf foo
|
||||||
|
}}`
|
||||||
|
|
||||||
|
var inp23 = `cmd compress ${{
|
||||||
|
mkdir "$1"
|
||||||
|
IFS=':'; cp ${fs} "$1"
|
||||||
|
tar -czvf "$1.tar.gz" "$1"
|
||||||
|
rm -rf "$1"
|
||||||
|
}}`
|
||||||
|
|
||||||
|
// unfinished command
|
||||||
|
var inp24 = `cmd compress ${{
|
||||||
|
mkdir "$1"`
|
||||||
|
|
||||||
|
var out0 = []string{}
|
||||||
|
var out1 = []string{}
|
||||||
|
var out2 = []string{"set", "hidden", "\n"}
|
||||||
|
var out3 = []string{"set", "hidden", ";", "set", "preview", "\n"}
|
||||||
|
var out4 = []string{"set", "ratios", "1:2:3", "\n"}
|
||||||
|
var out5 = []string{"set", "ratios", "1:2:3", ";"}
|
||||||
|
var out6 = []string{":", "set", "ratios", "1:2:3", "\n", "\n"}
|
||||||
|
var out7 = []string{":", "set", "ratios", "1:2:3", ";", "\n"}
|
||||||
|
var out8 = []string{"map", "gh", "cd", "~", "\n"}
|
||||||
|
var out9 = []string{"map", "gh", "cd", "~", ";"}
|
||||||
|
var out10 = []string{"map", "gh", ":", "cd", "~", "\n", "\n"}
|
||||||
|
var out11 = []string{"map", "gh", ":", "cd", "~", ";", "\n"}
|
||||||
|
var out12 = []string{"cmd", "usage", "$", "du -h . | less", "\n"}
|
||||||
|
var out13 = []string{"map", "u", "usage", "\n"}
|
||||||
|
var out14 = []string{"map", "u", "usage", ";"}
|
||||||
|
var out15 = []string{"map", "u", ":", "usage", "\n", "\n"}
|
||||||
|
var out16 = []string{"map", "u", ":", "usage", ";", "\n"}
|
||||||
|
var out17 = []string{"map", "u", "$", "du -h . | less", "\n"}
|
||||||
|
var out18 = []string{"cmd", "usage", "$", `du -h "$1" | less`, "\n"}
|
||||||
|
var out19 = []string{"map", "u", "usage", "/", "\n"}
|
||||||
|
var out20 = []string{"cmd", "gohome", ":", "{{", "cd", "~", "\n", "set", "hidden", "\n", "}}", "\n"}
|
||||||
|
var out21 = []string{"map", "gh", ":", "{{", "cd", "~", "\n", "set", "hidden", "\n", "}}", "\n"}
|
||||||
|
var out22 = []string{"map", "c", "$", "{{", "\n\tmkdir foo\n\tIFS=':'; cp ${fs} foo\n\ttar -czvf \"foo.tar.gz\" foo\n\trm -rf foo\n", "}}", "\n"}
|
||||||
|
var out23 = []string{"cmd", "compress", "$", "{{", "\n\tmkdir \"$1\"\n\tIFS=':'; cp ${fs} \"$1\"\n\ttar -czvf \"$1.tar.gz\" \"$1\"\n\trm -rf \"$1\"\n", "}}", "\n"}
|
||||||
|
var out24 = []string{"cmd", "compress", "$", "{{"}
|
||||||
|
|
||||||
|
func compare(t *testing.T, inp string, out []string) {
|
||||||
|
s := newScanner(strings.NewReader(inp))
|
||||||
|
|
||||||
|
for _, tok := range out {
|
||||||
|
if s.scan(); s.tok != tok {
|
||||||
|
t.Errorf("at input '%s' expected '%s' but scanned '%s'", inp, tok, s.tok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.scan() {
|
||||||
|
t.Errorf("at input '%s' unexpected '%s'", inp, s.tok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScan(t *testing.T) {
|
||||||
|
compare(t, inp0, out0)
|
||||||
|
compare(t, inp1, out1)
|
||||||
|
compare(t, inp2, out2)
|
||||||
|
compare(t, inp3, out3)
|
||||||
|
compare(t, inp4, out4)
|
||||||
|
compare(t, inp5, out5)
|
||||||
|
compare(t, inp6, out6)
|
||||||
|
compare(t, inp7, out7)
|
||||||
|
compare(t, inp8, out8)
|
||||||
|
compare(t, inp9, out9)
|
||||||
|
compare(t, inp10, out10)
|
||||||
|
compare(t, inp11, out11)
|
||||||
|
compare(t, inp12, out12)
|
||||||
|
compare(t, inp13, out13)
|
||||||
|
compare(t, inp14, out14)
|
||||||
|
compare(t, inp15, out15)
|
||||||
|
compare(t, inp16, out16)
|
||||||
|
compare(t, inp17, out17)
|
||||||
|
compare(t, inp18, out18)
|
||||||
|
compare(t, inp19, out19)
|
||||||
|
compare(t, inp20, out20)
|
||||||
|
compare(t, inp21, out21)
|
||||||
|
compare(t, inp22, out22)
|
||||||
|
compare(t, inp23, out23)
|
||||||
|
compare(t, inp24, out24)
|
||||||
|
}
|
94
server.go
Normal file
94
server.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
gKeepFile bool
|
||||||
|
gFileList []string
|
||||||
|
)
|
||||||
|
|
||||||
|
func serve() {
|
||||||
|
logFile, err := os.Create(gServerLogPath)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer logFile.Close()
|
||||||
|
log.SetOutput(logFile)
|
||||||
|
|
||||||
|
log.Print("hi!")
|
||||||
|
|
||||||
|
l, err := net.Listen("unix", gSocketPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("listening socket: %s", err)
|
||||||
|
}
|
||||||
|
defer l.Close()
|
||||||
|
|
||||||
|
listen(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
func listen(l net.Listener) {
|
||||||
|
for {
|
||||||
|
c, err := l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("accepting connection: %s", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
s := bufio.NewScanner(c)
|
||||||
|
|
||||||
|
for s.Scan() {
|
||||||
|
switch s.Text() {
|
||||||
|
case "save":
|
||||||
|
saveFilesServer(s)
|
||||||
|
log.Printf("listen: save, list: %v, keep: %t", gFileList, gKeepFile)
|
||||||
|
case "load":
|
||||||
|
loadFilesServer(c)
|
||||||
|
log.Printf("listen: load, keep: %t", gKeepFile)
|
||||||
|
default:
|
||||||
|
log.Print("listen: unexpected command")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveFilesServer(s *bufio.Scanner) {
|
||||||
|
switch s.Scan(); s.Text() {
|
||||||
|
case "keep":
|
||||||
|
gKeepFile = true
|
||||||
|
case "move":
|
||||||
|
gKeepFile = false
|
||||||
|
default:
|
||||||
|
log.Printf("unexpected option to keep file(s): %s", s.Text())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gFileList = nil
|
||||||
|
for s.Scan() {
|
||||||
|
gFileList = append(gFileList, s.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Err() != nil {
|
||||||
|
log.Printf("scanning: %s", s.Err())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadFilesServer(c net.Conn) {
|
||||||
|
if gKeepFile {
|
||||||
|
fmt.Fprintln(c, "keep")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintln(c, "move")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range gFileList {
|
||||||
|
fmt.Fprintln(c, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Close()
|
||||||
|
}
|
487
ui.go
Normal file
487
ui.go
Normal file
@ -0,0 +1,487 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/nsf/termbox-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Win struct {
|
||||||
|
w int
|
||||||
|
h int
|
||||||
|
x int
|
||||||
|
y int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWin(w, h, x, y int) *Win {
|
||||||
|
return &Win{w, h, x, y}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (win *Win) renew(w, h, x, y int) {
|
||||||
|
win.w = w
|
||||||
|
win.h = h
|
||||||
|
win.x = x
|
||||||
|
win.y = y
|
||||||
|
}
|
||||||
|
|
||||||
|
func (win *Win) print(x, y int, fg, bg termbox.Attribute, s string) {
|
||||||
|
off := x
|
||||||
|
for _, c := range s {
|
||||||
|
if x >= win.w {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
termbox.SetCell(win.x+x, win.y+y, c, fg, bg)
|
||||||
|
|
||||||
|
if c == '\t' {
|
||||||
|
x += gOpts.tabstop - (x-off)%gOpts.tabstop
|
||||||
|
} else {
|
||||||
|
x++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (win *Win) printf(x, y int, fg, bg termbox.Attribute, format string, a ...interface{}) {
|
||||||
|
win.print(x, y, fg, bg, fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (win *Win) printl(x, y int, fg, bg termbox.Attribute, s string) {
|
||||||
|
win.printf(x, y, fg, bg, "%s%*s", s, win.w-len(s), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (win *Win) printd(dir *Dir, marks map[string]bool) {
|
||||||
|
if win.w < 3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fg, bg := termbox.ColorDefault, termbox.ColorDefault
|
||||||
|
|
||||||
|
if len(dir.fi) == 0 {
|
||||||
|
fg = termbox.AttrBold
|
||||||
|
win.print(0, 0, fg, bg, "empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
maxind := len(dir.fi) - 1
|
||||||
|
|
||||||
|
beg := max(dir.ind-dir.pos, 0)
|
||||||
|
end := min(beg+win.h, maxind+1)
|
||||||
|
|
||||||
|
for i, f := range dir.fi[beg:end] {
|
||||||
|
switch {
|
||||||
|
case f.Mode().IsRegular():
|
||||||
|
if f.Mode()&0111 != 0 {
|
||||||
|
fg = termbox.AttrBold | termbox.ColorGreen
|
||||||
|
} else {
|
||||||
|
fg = termbox.ColorDefault
|
||||||
|
}
|
||||||
|
case f.Mode().IsDir():
|
||||||
|
fg = termbox.AttrBold | termbox.ColorBlue
|
||||||
|
case f.Mode()&os.ModeSymlink != 0:
|
||||||
|
fg = termbox.ColorCyan
|
||||||
|
case f.Mode()&os.ModeNamedPipe != 0:
|
||||||
|
fg = termbox.ColorRed
|
||||||
|
case f.Mode()&os.ModeSocket != 0:
|
||||||
|
fg = termbox.ColorYellow
|
||||||
|
case f.Mode()&os.ModeDevice != 0:
|
||||||
|
fg = termbox.ColorWhite
|
||||||
|
}
|
||||||
|
|
||||||
|
path := path.Join(dir.path, f.Name())
|
||||||
|
|
||||||
|
if marks[path] {
|
||||||
|
win.print(0, i, fg, termbox.ColorMagenta, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == dir.pos {
|
||||||
|
fg = fg | termbox.AttrReverse
|
||||||
|
}
|
||||||
|
|
||||||
|
var s []byte
|
||||||
|
|
||||||
|
s = append(s, ' ')
|
||||||
|
|
||||||
|
s = append(s, f.Name()...)
|
||||||
|
|
||||||
|
if len(s) > win.w-2 {
|
||||||
|
s = s[:win.w-2]
|
||||||
|
} else {
|
||||||
|
s = append(s, make([]byte, win.w-2-len(s))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch gOpts.showinfo {
|
||||||
|
case "none":
|
||||||
|
break
|
||||||
|
case "size":
|
||||||
|
if win.w > 8 {
|
||||||
|
h := humanize(f.Size())
|
||||||
|
s = append(s[:win.w-3-len(h)])
|
||||||
|
s = append(s, ' ')
|
||||||
|
s = append(s, h...)
|
||||||
|
}
|
||||||
|
case "time":
|
||||||
|
if win.w > 24 {
|
||||||
|
t := f.ModTime().Format("Jan _2 15:04")
|
||||||
|
s = append(s[:win.w-3-len(t)])
|
||||||
|
s = append(s, ' ')
|
||||||
|
s = append(s, t...)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log.Printf("unknown showinfo type: %s", gOpts.showinfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add a trailing '~' to the name if cut
|
||||||
|
|
||||||
|
win.print(1, i, fg, bg, string(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (win *Win) printr(reg *os.File) error {
|
||||||
|
fg, bg := termbox.ColorDefault, termbox.ColorDefault
|
||||||
|
|
||||||
|
buf := bufio.NewScanner(reg)
|
||||||
|
|
||||||
|
for i := 0; i < win.h && buf.Scan(); i++ {
|
||||||
|
for _, r := range buf.Text() {
|
||||||
|
if unicode.IsSpace(r) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !unicode.IsPrint(r) {
|
||||||
|
fg = termbox.AttrBold
|
||||||
|
win.print(0, 0, fg, bg, "binary")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf.Err() != nil {
|
||||||
|
return fmt.Errorf("printing regular file: %s", buf.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
reg.Seek(0, 0)
|
||||||
|
|
||||||
|
buf = bufio.NewScanner(reg)
|
||||||
|
|
||||||
|
for i := 0; i < win.h && buf.Scan(); i++ {
|
||||||
|
win.print(2, i, fg, bg, buf.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf.Err() != nil {
|
||||||
|
return fmt.Errorf("printing regular file: %s", buf.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type UI struct {
|
||||||
|
wins []*Win
|
||||||
|
pwdwin *Win
|
||||||
|
msgwin *Win
|
||||||
|
menuwin *Win
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWidths(wtot int) []int {
|
||||||
|
rsum := 0
|
||||||
|
for _, rat := range gOpts.ratios {
|
||||||
|
rsum += rat
|
||||||
|
}
|
||||||
|
|
||||||
|
wlen := len(gOpts.ratios)
|
||||||
|
widths := make([]int, wlen)
|
||||||
|
|
||||||
|
wsum := 0
|
||||||
|
for i := 0; i < wlen-1; i++ {
|
||||||
|
widths[i] = gOpts.ratios[i] * (wtot / rsum)
|
||||||
|
wsum += widths[i]
|
||||||
|
}
|
||||||
|
widths[wlen-1] = wtot - wsum
|
||||||
|
|
||||||
|
return widths
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUI() *UI {
|
||||||
|
wtot, htot := termbox.Size()
|
||||||
|
|
||||||
|
var wins []*Win
|
||||||
|
|
||||||
|
widths := getWidths(wtot)
|
||||||
|
|
||||||
|
wacc := 0
|
||||||
|
wlen := len(widths)
|
||||||
|
for i := 0; i < wlen; i++ {
|
||||||
|
wins = append(wins, newWin(widths[i], htot-2, wacc, 1))
|
||||||
|
wacc += widths[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return &UI{
|
||||||
|
wins: wins,
|
||||||
|
pwdwin: newWin(wtot, 1, 0, 0),
|
||||||
|
msgwin: newWin(wtot, 1, 0, htot-1),
|
||||||
|
menuwin: newWin(wtot, 1, 0, htot-2),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UI) renew() {
|
||||||
|
termbox.Flush()
|
||||||
|
|
||||||
|
wtot, htot := termbox.Size()
|
||||||
|
|
||||||
|
widths := getWidths(wtot)
|
||||||
|
|
||||||
|
wacc := 0
|
||||||
|
wlen := len(widths)
|
||||||
|
for i := 0; i < wlen; i++ {
|
||||||
|
ui.wins[i].renew(widths[i], htot-2, wacc, 1)
|
||||||
|
wacc += widths[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.msgwin.renew(wtot, 1, 0, htot-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UI) echoFileInfo(nav *Nav) {
|
||||||
|
dir := nav.currDir()
|
||||||
|
|
||||||
|
if len(dir.fi) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
curr := nav.currFile()
|
||||||
|
|
||||||
|
ui.message = fmt.Sprintf("%v %v %v", curr.Mode(), humanize(curr.Size()), curr.ModTime().Format(time.ANSIC))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UI) clearMsg() {
|
||||||
|
fg, bg := termbox.ColorDefault, termbox.ColorDefault
|
||||||
|
win := ui.msgwin
|
||||||
|
win.printl(0, 0, fg, bg, "")
|
||||||
|
termbox.SetCursor(win.x, win.y)
|
||||||
|
termbox.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UI) draw(nav *Nav) {
|
||||||
|
fg, bg := termbox.ColorDefault, termbox.ColorDefault
|
||||||
|
|
||||||
|
termbox.Clear(fg, bg)
|
||||||
|
defer termbox.Flush()
|
||||||
|
|
||||||
|
dir := nav.currDir()
|
||||||
|
|
||||||
|
path := strings.Replace(dir.path, envHome, "~", -1)
|
||||||
|
|
||||||
|
ui.pwdwin.printf(0, 0, termbox.AttrBold|termbox.ColorGreen, bg, "%s@%s", envUser, envHost)
|
||||||
|
ui.pwdwin.printf(len(envUser)+len(envHost)+1, 0, fg, bg, ":")
|
||||||
|
ui.pwdwin.printf(len(envUser)+len(envHost)+2, 0, termbox.AttrBold|termbox.ColorBlue, bg, "%s", path)
|
||||||
|
|
||||||
|
length := min(len(ui.wins), len(nav.dirs))
|
||||||
|
woff := len(ui.wins) - length
|
||||||
|
|
||||||
|
if gOpts.preview {
|
||||||
|
length = min(len(ui.wins)-1, len(nav.dirs))
|
||||||
|
woff = len(ui.wins) - 1 - length
|
||||||
|
}
|
||||||
|
|
||||||
|
doff := len(nav.dirs) - length
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
ui.wins[woff+i].printd(nav.dirs[doff+i], nav.marks)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gOpts.preview {
|
||||||
|
if len(dir.fi) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
preview := ui.wins[len(ui.wins)-1]
|
||||||
|
curr := nav.currFile()
|
||||||
|
path := nav.currPath()
|
||||||
|
if curr.IsDir() {
|
||||||
|
dir := newDir(path)
|
||||||
|
dir.load(nav.inds[path], nav.poss[path], nav.height, nav.names[path])
|
||||||
|
preview.printd(dir, nav.marks)
|
||||||
|
} else {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
ui.message = err.Error()
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := preview.printr(file); err != nil {
|
||||||
|
ui.message = err.Error()
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.msgwin.print(0, 0, fg, bg, ui.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findBinds(keys map[string]Expr, prefix string) (binds map[string]Expr, ok bool) {
|
||||||
|
binds = make(map[string]Expr)
|
||||||
|
for key, expr := range keys {
|
||||||
|
if strings.HasPrefix(key, prefix) {
|
||||||
|
binds[key] = expr
|
||||||
|
if key == prefix {
|
||||||
|
ok = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UI) getExpr() Expr {
|
||||||
|
r := &CallExpr{"redraw", nil}
|
||||||
|
|
||||||
|
var acc []rune
|
||||||
|
|
||||||
|
for {
|
||||||
|
switch ev := termbox.PollEvent(); ev.Type {
|
||||||
|
case termbox.EventKey:
|
||||||
|
if ev.Ch != 0 {
|
||||||
|
acc = append(acc, ev.Ch)
|
||||||
|
} else {
|
||||||
|
// TODO: rest of the keys
|
||||||
|
switch ev.Key {
|
||||||
|
case termbox.KeySpace:
|
||||||
|
acc = append(acc, '<', 's', 'p', 'a', 'c', 'e', '>')
|
||||||
|
case termbox.KeyEnter:
|
||||||
|
acc = append(acc, '<', 'c', 'r', '>')
|
||||||
|
case termbox.KeyBackspace:
|
||||||
|
acc = append(acc, '<', 'b', 's', '>')
|
||||||
|
case termbox.KeyBackspace2:
|
||||||
|
acc = append(acc, '<', 'b', 's', '2', '>')
|
||||||
|
case termbox.KeyTab:
|
||||||
|
acc = append(acc, '<', 't', 'a', 'b', '>')
|
||||||
|
case termbox.KeyCtrlL:
|
||||||
|
acc = append(acc, '<', 'c', '-', 'l', '>')
|
||||||
|
case termbox.KeyEsc:
|
||||||
|
acc = nil
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binds, ok := findBinds(gOpts.keys, string(acc))
|
||||||
|
|
||||||
|
switch len(binds) {
|
||||||
|
case 0:
|
||||||
|
ui.message = fmt.Sprintf("unknown mapping: %s", string(acc))
|
||||||
|
acc = nil
|
||||||
|
return r
|
||||||
|
case 1:
|
||||||
|
if ok {
|
||||||
|
return gOpts.keys[string(acc)]
|
||||||
|
}
|
||||||
|
ui.listBinds(binds)
|
||||||
|
default:
|
||||||
|
if ok {
|
||||||
|
// TODO: use a delay
|
||||||
|
return gOpts.keys[string(acc)]
|
||||||
|
}
|
||||||
|
ui.listBinds(binds)
|
||||||
|
}
|
||||||
|
case termbox.EventResize:
|
||||||
|
return r
|
||||||
|
default:
|
||||||
|
// TODO: handle other events
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UI) prompt(pref string) string {
|
||||||
|
fg, bg := termbox.ColorDefault, termbox.ColorDefault
|
||||||
|
|
||||||
|
win := ui.msgwin
|
||||||
|
|
||||||
|
win.printl(0, 0, fg, bg, pref)
|
||||||
|
termbox.SetCursor(win.x+len(pref), win.y)
|
||||||
|
defer termbox.HideCursor()
|
||||||
|
termbox.Flush()
|
||||||
|
|
||||||
|
var acc []rune
|
||||||
|
|
||||||
|
for {
|
||||||
|
switch ev := termbox.PollEvent(); ev.Type {
|
||||||
|
case termbox.EventKey:
|
||||||
|
if ev.Ch != 0 {
|
||||||
|
acc = append(acc, ev.Ch)
|
||||||
|
} else {
|
||||||
|
// TODO: rest of the keys
|
||||||
|
switch ev.Key {
|
||||||
|
case termbox.KeySpace:
|
||||||
|
acc = append(acc, ' ')
|
||||||
|
case termbox.KeyBackspace2:
|
||||||
|
if len(acc) > 0 {
|
||||||
|
acc = acc[:len(acc)-1]
|
||||||
|
}
|
||||||
|
case termbox.KeyEnter:
|
||||||
|
win.printl(0, 0, fg, bg, "")
|
||||||
|
termbox.SetCursor(win.x, win.y)
|
||||||
|
termbox.Flush()
|
||||||
|
return string(acc)
|
||||||
|
case termbox.KeyEsc:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
win.printl(0, 0, fg, bg, pref)
|
||||||
|
win.print(len(pref), 0, fg, bg, string(acc))
|
||||||
|
termbox.SetCursor(win.x+len(pref)+len(acc), win.y)
|
||||||
|
termbox.Flush()
|
||||||
|
default:
|
||||||
|
// TODO: handle other events
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UI) pause() {
|
||||||
|
termbox.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UI) resume() {
|
||||||
|
if err := termbox.Init(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UI) sync() {
|
||||||
|
err := termbox.Sync()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("syncing: %s", err)
|
||||||
|
}
|
||||||
|
termbox.SetCursor(0, 0)
|
||||||
|
termbox.HideCursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UI) listBinds(binds map[string]Expr) {
|
||||||
|
t := new(tabwriter.Writer)
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
|
||||||
|
t.Init(b, 0, 8, 0, '\t', 0)
|
||||||
|
fmt.Fprintln(t, "keys\tcommand")
|
||||||
|
for key, expr := range binds {
|
||||||
|
fmt.Fprintf(t, "%s\t%v\n", key, expr)
|
||||||
|
}
|
||||||
|
t.Flush()
|
||||||
|
|
||||||
|
lines := strings.Split(b.String(), "\n")
|
||||||
|
|
||||||
|
lines = lines[:len(lines)-1]
|
||||||
|
|
||||||
|
ui.menuwin.h = len(lines) - 1
|
||||||
|
ui.menuwin.y = ui.wins[0].h - ui.menuwin.h
|
||||||
|
|
||||||
|
ui.menuwin.printl(0, 0, termbox.AttrBold, termbox.AttrBold, lines[0])
|
||||||
|
for i, line := range lines[1:] {
|
||||||
|
ui.menuwin.printl(0, i+1, termbox.ColorDefault, termbox.ColorDefault, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
termbox.Flush()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user