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