* initial tags implementation

* add tag to complete.go

* add gTagsPath to os_windows.go

* change behaviour to match other commands

* add tag-toggle to complete.go

* add tag and tag-toggle to docs

* address feedback about tags
This commit is contained in:
Santos 2022-03-28 14:43:05 +02:00 committed by GitHub
parent 2f9d840eda
commit 5a5628d667
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 440 additions and 205 deletions

View File

@ -51,6 +51,10 @@ func run() {
app.ui.echoerrf("reading marks file: %s", err) app.ui.echoerrf("reading marks file: %s", err)
} }
if err := app.nav.readTags(); err != nil {
app.ui.echoerrf("reading tags file: %s", err)
}
if err := app.readHistory(); err != nil { if err := app.readHistory(); err != nil {
app.ui.echoerrf("reading history file: %s", err) app.ui.echoerrf("reading history file: %s", err)
} }

View File

@ -69,6 +69,8 @@ var (
"source", "source",
"push", "push",
"delete", "delete",
"tag",
"tag-toggle",
} }
gOptWords = []string{ gOptWords = []string{

20
doc.go
View File

@ -71,6 +71,8 @@ The following commands are provided by lf:
mark-save (modal) (default 'm') mark-save (modal) (default 'm')
mark-load (modal) (default "'") mark-load (modal) (default "'")
mark-remove (modal) (default `"`) mark-remove (modal) (default `"`)
tag-toggle (default t)
tag
The following command line commands are provided by lf: The following command line commands are provided by lf:
@ -149,6 +151,7 @@ The following options can be used to customize the behavior of lf:
waitmsg string (default 'Press any key to continue') waitmsg string (default 'Press any key to continue')
wrapscan bool (default on) wrapscan bool (default on)
wrapscroll bool (default off) wrapscroll bool (default off)
tagfmt string (default "\033[31m%s\033[0m")
The following environment variables are exported for shell commands: The following environment variables are exported for shell commands:
@ -233,6 +236,11 @@ History file should be located at:
unix ~/.local/share/lf/history unix ~/.local/share/lf/history
windows C:\Users\<user>\AppData\Local\lf\history windows C:\Users\<user>\AppData\Local\lf\history
Tags file should be located at:
unix ~/.local/share/lf/tags
windows C:\Users\<user>\AppData\Local\lf\tags
You can configure the default values of following variables to change these You can configure the default values of following variables to change these
locations: locations:
@ -457,6 +465,14 @@ A special bookmark "'" holds the previous directory after a 'mark-load', 'cd', o
Remove a bookmark assigned to the given key. Remove a bookmark assigned to the given key.
tag
Tag a file with a single width character given in the argument.
tag-toggle (modal) (default 't')
Tag a file with a single width character given in the argument if the file is untagged, otherwise remove the tag.
Command Line Commands Command Line Commands
This section shows information about command line commands. This section shows information about command line commands.
@ -782,6 +798,10 @@ Searching can wrap around the file list.
Scrolling can wrap around the file list. Scrolling can wrap around the file list.
tagfmt string (default "\033[31m%s\033[0m")
Format string of the tags.
Environment Variables Environment Variables
The following variables are exported for shell commands: The following variables are exported for shell commands:

View File

@ -76,6 +76,8 @@ The following commands are provided by lf:
mark-save (modal) (default 'm') mark-save (modal) (default 'm')
mark-load (modal) (default "'") mark-load (modal) (default "'")
mark-remove (modal) (default '"') mark-remove (modal) (default '"')
tag-toggle (default t)
tag
The following command line commands are provided by lf: The following command line commands are provided by lf:
@ -154,6 +156,7 @@ The following options can be used to customize the behavior of lf:
waitmsg string (default 'Press any key to continue') waitmsg string (default 'Press any key to continue')
wrapscan bool (default on) wrapscan bool (default on)
wrapscroll bool (default off) wrapscroll bool (default off)
tagfmt string (default "\033[31m%s\033[0m")
The following environment variables are exported for shell commands: The following environment variables are exported for shell commands:
@ -240,6 +243,11 @@ History file should be located at:
unix ~/.local/share/lf/history unix ~/.local/share/lf/history
windows C:\Users\<user>\AppData\Local\lf\history windows C:\Users\<user>\AppData\Local\lf\history
Tags file should be located at:
unix ~/.local/share/lf/tags
windows C:\Users\<user>\AppData\Local\lf\tags
You can configure the default values of following variables to change these You can configure the default values of following variables to change these
locations: locations:
@ -482,6 +490,15 @@ or 'select' command.
Remove a bookmark assigned to the given key. Remove a bookmark assigned to the given key.
tag
Tag a file with a single width character given in the argument.
tag-toggle (modal) (default 't')
Tag a file with a single width character given in the argument if the file
is untagged, otherwise remove the tag.
Command Line Commands Command Line Commands
@ -839,6 +856,10 @@ Searching can wrap around the file list.
Scrolling can wrap around the file list. Scrolling can wrap around the file list.
tagfmt string (default "\033[31m%s\033[0m")
Format string of the tags.
Environment Variables Environment Variables

44
eval.go
View File

@ -983,6 +983,50 @@ func (e *callExpr) eval(app *app, args []string) {
} }
} }
} }
case "tag-toggle":
if !app.nav.init {
return
}
if len(e.args) == 0 {
app.ui.echoerr("tag-toggle: missing default tag")
} else if err := app.nav.toggleTag(e.args[0]); err != nil {
app.ui.echoerrf("tag-toggle: %s", err)
} else if err := app.nav.writeTags(); err != nil {
app.ui.echoerrf("tag-toggle: %s", err)
}
if gSingleMode {
if err := app.nav.sync(); err != nil {
app.ui.echoerrf("tag-toggle: %s", err)
}
} else {
if err := remote("send sync"); err != nil {
app.ui.echoerrf("tag-toggle: %s", err)
}
}
case "tag":
if !app.nav.init {
return
}
if len(e.args) == 0 {
app.ui.echoerr("tag: missing tag")
} else if err := app.nav.tag(e.args[0]); err != nil {
app.ui.echoerrf("tag: %s", err)
} else if err := app.nav.writeTags(); err != nil {
app.ui.echoerrf("tag: %s", err)
}
if gSingleMode {
if err := app.nav.sync(); err != nil {
app.ui.echoerrf("tag: %s", err)
}
} else {
if err := remote("send sync"); err != nil {
app.ui.echoerrf("tag: %s", err)
}
}
case "invert": case "invert":
if !app.nav.init { if !app.nav.init {
return return

28
lf.1
View File

@ -86,6 +86,8 @@ The following commands are provided by lf:
mark-save (modal) (default 'm') mark-save (modal) (default 'm')
mark-load (modal) (default "'") mark-load (modal) (default "'")
mark-remove (modal) (default `"`) mark-remove (modal) (default `"`)
tag-toggle (default t)
tag
.EE .EE
.PP .PP
The following command line commands are provided by lf: The following command line commands are provided by lf:
@ -168,6 +170,7 @@ The following options can be used to customize the behavior of lf:
waitmsg string (default 'Press any key to continue') waitmsg string (default 'Press any key to continue')
wrapscan bool (default on) wrapscan bool (default on)
wrapscroll bool (default off) wrapscroll bool (default off)
tagfmt string (default "\e033[31m%s\e033[0m")
.EE .EE
.PP .PP
The following environment variables are exported for shell commands: The following environment variables are exported for shell commands:
@ -271,6 +274,13 @@ History file should be located at:
windows C:\eUsers\e<user>\eAppData\eLocal\elf\ehistory windows C:\eUsers\e<user>\eAppData\eLocal\elf\ehistory
.EE .EE
.PP .PP
Tags file should be located at:
.PP
.EX
unix ~/.local/share/lf/tags
windows C:\eUsers\e<user>\eAppData\eLocal\elf\etags
.EE
.PP
You can configure the default values of following variables to change these locations: You can configure the default values of following variables to change these locations:
.PP .PP
.EX .EX
@ -564,6 +574,18 @@ Change the current directory to the bookmark assigned to the given key. A specia
.EE .EE
.PP .PP
Remove a bookmark assigned to the given key. Remove a bookmark assigned to the given key.
.PP
.EX
tag
.EE
.PP
Tag a file with a single width character given in the argument.
.PP
.EX
tag-toggle (modal) (default 't')
.EE
.PP
Tag a file with a single width character given in the argument if the file is untagged, otherwise remove the tag.
.SH COMMAND LINE COMMANDS .SH COMMAND LINE COMMANDS
This section shows information about command line commands. These should be mostly compatible with readline keybindings. A character refers to a unicode code point, a word consists of letters and digits, and a unix word consists of any non-blank characters. This section shows information about command line commands. These should be mostly compatible with readline keybindings. A character refers to a unicode code point, a word consists of letters and digits, and a unix word consists of any non-blank characters.
.PP .PP
@ -955,6 +977,12 @@ Searching can wrap around the file list.
.EE .EE
.PP .PP
Scrolling can wrap around the file list. Scrolling can wrap around the file list.
.PP
.EX
tagfmt string (default "\e033[31m%s\e033[0m")
.EE
.PP
Format string of the tags.
.SH ENVIRONMENT VARIABLES .SH ENVIRONMENT VARIABLES
The following variables are exported for shell commands: These are referred with a '$' prefix on POSIX shells (e.g. '$f'), between '%' characters on Windows cmd (e.g. '%f%'), and with a '$env:' prefix on Windows powershell (e.g. '$env:f'). The following variables are exported for shell commands: These are referred with a '$' prefix on POSIX shells (e.g. '$f'), between '%' characters on Windows cmd (e.g. '%f%'), and with a '$env:' prefix on Windows powershell (e.g. '$env:f').
.PP .PP

97
nav.go
View File

@ -61,6 +61,7 @@ func readdir(path string) ([]*file, error) {
fpath := filepath.Join(path, fname) fpath := filepath.Join(path, fname)
lstat, err := os.Lstat(fpath) lstat, err := os.Lstat(fpath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
continue continue
} }
@ -373,6 +374,7 @@ type nav struct {
renameOldPath string renameOldPath string
renameNewPath string renameNewPath string
selections map[string]int selections map[string]int
tags map[string]string
selectionInd int selectionInd int
height int height int
find string find string
@ -495,6 +497,7 @@ func newNav(height int) *nav {
saves: make(map[string]bool), saves: make(map[string]bool),
marks: make(map[string]string), marks: make(map[string]string),
selections: make(map[string]int), selections: make(map[string]int),
tags: make(map[string]string),
selectionInd: 0, selectionInd: 0,
height: height, height: height,
jumpList: make([]string, 0), jumpList: make([]string, 0),
@ -961,6 +964,46 @@ func (nav *nav) toggle() {
nav.toggleSelection(curr.path) nav.toggleSelection(curr.path)
} }
func (nav *nav) toggleTagSelection(path string, tag string) {
if _, ok := nav.tags[path]; ok {
delete(nav.tags, path)
} else {
nav.tags[path] = tag
}
}
func (nav *nav) toggleTag(tag string) error {
list, err := nav.currFileOrSelections()
if err != nil {
return err
}
if printLength(tag) != 1 {
return errors.New("tag should be single width character")
}
for _, path := range list {
nav.toggleTagSelection(path, tag)
}
return nil
}
func (nav *nav) tag(tag string) error {
curr, err := nav.currFile()
if err != nil {
return err
}
if printLength(tag) != 1 {
return errors.New("tag should be single width character")
}
nav.tags[curr.path] = tag
return nil
}
func (nav *nav) invert() { func (nav *nav) invert() {
dir := nav.currDir() dir := nav.currDir()
for _, f := range dir.files { for _, f := range dir.files {
@ -1268,6 +1311,7 @@ func (nav *nav) sync() error {
nav.marks[tmp] = v nav.marks[tmp] = v
} }
} }
err = nav.readTags()
return err return err
} }
@ -1561,6 +1605,59 @@ func (nav *nav) writeMarks() error {
return nil return nil
} }
func (nav *nav) readTags() error {
nav.tags = make(map[string]string)
f, err := os.Open(gTagsPath)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return fmt.Errorf("opening tags file: %s", err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
toks := strings.SplitN(scanner.Text(), ":", 2)
if _, ok := nav.tags[toks[0]]; !ok {
nav.tags[toks[0]] = toks[1]
}
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("reading tags file: %s", err)
}
return nil
}
func (nav *nav) writeTags() error {
if err := os.MkdirAll(filepath.Dir(gTagsPath), os.ModePerm); err != nil {
return fmt.Errorf("creating data directory: %s", err)
}
f, err := os.Create(gTagsPath)
if err != nil {
return fmt.Errorf("creating tags file: %s", err)
}
defer f.Close()
var keys []string
for k := range nav.tags {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
_, err = f.WriteString(fmt.Sprintf("%s:%s\n", k, nav.tags[k]))
if err != nil {
return fmt.Errorf("writing tags file: %s", err)
}
}
return nil
}
func (nav *nav) currDir() *dir { func (nav *nav) currDir() *dir {
return nav.dirs[len(nav.dirs)-1] return nav.dirs[len(nav.dirs)-1]
} }

View File

@ -74,6 +74,7 @@ var gOpts struct {
cmds map[string]expr cmds map[string]expr
sortType sortType sortType sortType
tempmarks string tempmarks string
tagfmt string
} }
func init() { func init() {
@ -120,6 +121,7 @@ func init() {
gOpts.shellopts = nil gOpts.shellopts = nil
gOpts.sortType = sortType{naturalSort, dirfirstSort} gOpts.sortType = sortType{naturalSort, dirfirstSort}
gOpts.tempmarks = "'" gOpts.tempmarks = "'"
gOpts.tagfmt = "\033[31m%s\033[0m"
gOpts.keys = make(map[string]expr) gOpts.keys = make(map[string]expr)
@ -147,6 +149,7 @@ func init() {
gOpts.keys["["] = &callExpr{"jump-prev", nil, 1} gOpts.keys["["] = &callExpr{"jump-prev", nil, 1}
gOpts.keys["]"] = &callExpr{"jump-next", nil, 1} gOpts.keys["]"] = &callExpr{"jump-next", nil, 1}
gOpts.keys["<space>"] = &listExpr{[]expr{&callExpr{"toggle", nil, 1}, &callExpr{"down", nil, 1}}, 1} gOpts.keys["<space>"] = &listExpr{[]expr{&callExpr{"toggle", nil, 1}, &callExpr{"down", nil, 1}}, 1}
gOpts.keys["t"] = &listExpr{[]expr{&callExpr{"tag-toggle", []string{"*"}, 1}, &callExpr{"down", nil, 1}}, 1}
gOpts.keys["v"] = &callExpr{"invert", nil, 1} gOpts.keys["v"] = &callExpr{"invert", nil, 1}
gOpts.keys["u"] = &callExpr{"unselect", nil, 1} gOpts.keys["u"] = &callExpr{"unselect", nil, 1}
gOpts.keys["y"] = &callExpr{"copy", nil, 1} gOpts.keys["y"] = &callExpr{"copy", nil, 1}

2
os.go
View File

@ -35,6 +35,7 @@ var (
gIconsPaths []string gIconsPaths []string
gFilesPath string gFilesPath string
gMarksPath string gMarksPath string
gTagsPath string
gHistoryPath string gHistoryPath string
) )
@ -98,6 +99,7 @@ func init() {
gFilesPath = filepath.Join(data, "lf", "files") gFilesPath = filepath.Join(data, "lf", "files")
gMarksPath = filepath.Join(data, "lf", "marks") gMarksPath = filepath.Join(data, "lf", "marks")
gTagsPath = filepath.Join(data, "lf", "tags")
gHistoryPath = filepath.Join(data, "lf", "history") gHistoryPath = filepath.Join(data, "lf", "history")
runtime := os.Getenv("XDG_RUNTIME_DIR") runtime := os.Getenv("XDG_RUNTIME_DIR")

View File

@ -33,6 +33,7 @@ var (
gColorsPaths []string gColorsPaths []string
gIconsPaths []string gIconsPaths []string
gFilesPath string gFilesPath string
gTagsPath string
gMarksPath string gMarksPath string
gHistoryPath string gHistoryPath string
) )
@ -82,6 +83,7 @@ func init() {
gFilesPath = filepath.Join(data, "lf", "files") gFilesPath = filepath.Join(data, "lf", "files")
gMarksPath = filepath.Join(data, "lf", "marks") gMarksPath = filepath.Join(data, "lf", "marks")
gTagsPath = filepath.Join(data, "lf", "tags")
gHistoryPath = filepath.Join(data, "lf", "history") gHistoryPath = filepath.Join(data, "lf", "history")
} }

18
ui.go
View File

@ -324,7 +324,7 @@ func fileInfo(f *file, d *dir) string {
return info return info
} }
func (win *win) printDir(screen tcell.Screen, dir *dir, selections map[string]int, saves map[string]bool, colors styleMap, icons iconMap) { func (win *win) printDir(screen tcell.Screen, dir *dir, selections map[string]int, saves map[string]bool, tags map[string]string, colors styleMap, icons iconMap) {
if win.w < 5 || dir == nil { if win.w < 5 || dir == nil {
return return
} }
@ -449,6 +449,18 @@ func (win *win) printDir(screen tcell.Screen, dir *dir, selections map[string]in
s = append(s, ' ') s = append(s, ' ')
win.print(screen, lnwidth+1, i, st, string(s)) win.print(screen, lnwidth+1, i, st, string(s))
tag, ok := tags[path]
if ok {
st = st.Reverse(false)
fg, bg, _ := st.Decompose()
if i == dir.pos {
win.print(screen, lnwidth+1, i, st.Background(fg), fmt.Sprintf(gOpts.tagfmt, tag))
} else {
win.print(screen, lnwidth+1, i, st.Background(bg), fmt.Sprintf(gOpts.tagfmt, tag))
}
}
} }
} }
@ -839,7 +851,7 @@ func (ui *ui) draw(nav *nav) {
doff := len(nav.dirs) - length doff := len(nav.dirs) - length
for i := 0; i < length; i++ { for i := 0; i < length; i++ {
ui.wins[woff+i].printDir(ui.screen, nav.dirs[doff+i], nav.selections, nav.saves, ui.styles, ui.icons) ui.wins[woff+i].printDir(ui.screen, nav.dirs[doff+i], nav.selections, nav.saves, nav.tags, ui.styles, ui.icons)
} }
switch ui.cmdPrefix { switch ui.cmdPrefix {
@ -873,7 +885,7 @@ func (ui *ui) draw(nav *nav) {
preview := ui.wins[len(ui.wins)-1] preview := ui.wins[len(ui.wins)-1]
if curr.IsDir() { if curr.IsDir() {
preview.printDir(ui.screen, ui.dirPrev, nav.selections, nav.saves, ui.styles, ui.icons) preview.printDir(ui.screen, ui.dirPrev, nav.selections, nav.saves, nav.tags, ui.styles, ui.icons)
} else if curr.Mode().IsRegular() { } else if curr.Mode().IsRegular() {
preview.printReg(ui.screen, ui.regPrev) preview.printReg(ui.screen, ui.regPrev)
} }