package main import ( "os" "runtime" "strings" "github.com/anmitsu/goful/app" "github.com/anmitsu/goful/cmdline" "github.com/anmitsu/goful/filer" "github.com/anmitsu/goful/look" "github.com/anmitsu/goful/menu" "github.com/anmitsu/goful/message" "github.com/anmitsu/goful/widget" "github.com/mattn/go-runewidth" ) func main() { is_tmux := false widget.Init() defer widget.Fini() if runtime.GOOS == "darwin" { is_tmux = strings.Contains(os.Getenv("TERM_PROGRAM"), "tmux") } else { is_tmux = strings.Contains(os.Getenv("TERM"), "screen") } // Change a terminal title. if is_tmux { os.Stdout.WriteString("\033kgoful\033") // for tmux } else { os.Stdout.WriteString("\033]0;goful\007") // for otherwise } const state = "~/.goful/state.json" const history = "~/.goful/history/shell" goful := app.NewGoful(state) config(goful, is_tmux) _ = cmdline.LoadHistory(history) goful.Run() _ = goful.SaveState(state) _ = cmdline.SaveHistory(history) } func config(g *app.Goful, is_tmux bool) { look.Set("default") // default, midnight, black, white if runewidth.EastAsianWidth { // Because layout collapsing for ambiguous runes if LANG=ja_JP. widget.SetBorder('|', '-', '+', '+', '+', '+') } else { // Look good if environment variable RUNEWIDTH_EASTASIAN=0 and // ambiguous char setting is half-width for gnome-terminal. widget.SetBorder('│', '─', '┌', '┐', '└', '┘') // 0x2502, 0x2500, 0x250c, 0x2510, 0x2514, 0x2518 } g.SetBorderStyle(widget.AllBorder) // AllBorder, ULBorder, NoBorder message.SetInfoLog("~/.goful/log/info.log") // "" is not logging message.SetErrorLog("~/.goful/log/error.log") // "" is not logging message.Sec(5) // display second for a message // Setup widget keymaps. g.ConfigFiler(filerKeymap) filer.ConfigFinder(finderKeymap) cmdline.Config(cmdlineKeymap) cmdline.ConfigCompletion(completionKeymap) menu.Config(menuKeymap) filer.SetStatView(true, false, true) // size, permission and time filer.SetTimeFormat("06-01-02 15:04") // ex: "Jan _2 15:04" // Setup open command for C-m (when the enter key is pressed) // The macro %f means expanded to a file name, for more see (spawn.go) opener := "xdg-open %f %&" switch runtime.GOOS { case "windows": opener = "explorer %~f %&" case "darwin": opener = "open %f %&" } g.MergeKeymap(widget.Keymap{ "C-m": func() { g.Spawn(opener) }, "o": func() { g.Spawn(opener) }, }) // Setup pager by $PAGER pager := os.Getenv("PAGER") if pager == "" { if runtime.GOOS == "windows" { pager = "more" } else { pager = "less" } } if runtime.GOOS == "windows" { pager += " %~f" } else { pager += " %f" } g.AddKeymap("i", func() { g.Spawn(pager) }) // Setup a shell and a terminal to execute external commands. // The shell is called when execute on background by the macro %&. // The terminal is called when the other. if runtime.GOOS == "windows" { g.ConfigShell(func(cmd string) []string { return []string{"cmd", "/c", cmd} }) g.ConfigTerminal(func(cmd string) []string { return []string{"cmd", "/c", "start", "cmd", "/c", cmd + "& pause"} }) } else { g.ConfigShell(func(cmd string) []string { return []string{"bash", "-c", cmd} }) g.ConfigTerminal(func(cmd string) []string { // for not close the terminal when the shell finishes running const tail = `;read -p "HIT ENTER KEY"` if is_tmux { // such as screen and tmux return []string{"tmux", "new-window", "-n", cmd, cmd + tail} } // To execute bash in gnome-terminal of a new window or tab. title := "echo -n '\033]0;" + cmd + "\007';" // for change title return []string{"gnome-terminal", "--", "bash", "-c", title + cmd + tail} }) } // Setup menus and add to keymap. menu.Add("sort", "n", "sort name ", func() { g.Dir().SortName() }, "N", "sort name decending", func() { g.Dir().SortNameDec() }, "s", "sort size ", func() { g.Dir().SortSize() }, "S", "sort size decending", func() { g.Dir().SortSizeDec() }, "t", "sort time ", func() { g.Dir().SortMtime() }, "T", "sort time decending", func() { g.Dir().SortMtimeDec() }, "e", "sort ext ", func() { g.Dir().SortExt() }, "E", "sort ext decending ", func() { g.Dir().SortExtDec() }, ".", "toggle priority ", func() { filer.TogglePriority(); g.Workspace().ReloadAll() }, ) g.AddKeymap("s", func() { g.Menu("sort") }) menu.Add("view", "s", "stat menu ", func() { g.Menu("stat") }, "l", "layout menu ", func() { g.Menu("layout") }, "L", "look menu ", func() { g.Menu("look") }, ".", "toggle show hidden files", func() { filer.ToggleShowHiddens(); g.Workspace().ReloadAll() }, ) g.AddKeymap("v", func() { g.Menu("view") }) menu.Add("layout", "t", "tile ", func() { g.Workspace().LayoutTile() }, "T", "tile-top ", func() { g.Workspace().LayoutTileTop() }, "b", "tile-bottom", func() { g.Workspace().LayoutTileBottom() }, "r", "one-row ", func() { g.Workspace().LayoutOnerow() }, "c", "one-column ", func() { g.Workspace().LayoutOnecolumn() }, "f", "fullscreen ", func() { g.Workspace().LayoutFullscreen() }, ) menu.Add("stat", "s", "toggle size ", func() { filer.ToggleSizeView() }, "p", "toggle perm ", func() { filer.TogglePermView() }, "t", "toggle time ", func() { filer.ToggleTimeView() }, "1", "all stat ", func() { filer.SetStatView(true, true, true) }, "0", "no stat ", func() { filer.SetStatView(false, false, false) }, ) menu.Add("look", "d", "default ", func() { look.Set("default") }, "n", "midnight ", func() { look.Set("midnight") }, "b", "black ", func() { look.Set("black") }, "w", "white ", func() { look.Set("white") }, "a", "all border ", func() { g.SetBorderStyle(widget.AllBorder) }, "u", "ul border ", func() { g.SetBorderStyle(widget.ULBorder) }, "0", "no border ", func() { g.SetBorderStyle(widget.NoBorder) }, ) menu.Add("command", "c", "copy ", func() { g.Copy() }, "m", "move ", func() { g.Move() }, "D", "delete ", func() { g.Remove() }, "k", "mkdir ", func() { g.Mkdir() }, "n", "newfile ", func() { g.Touch() }, "M", "chmod ", func() { g.Chmod() }, "r", "rename ", func() { g.Rename() }, "R", "bulk rename ", func() { g.BulkRename() }, "d", "chdir ", func() { g.Chdir() }, "g", "glob ", func() { g.Glob() }, "G", "globdir ", func() { g.Globdir() }, ) g.AddKeymap("x", func() { g.Menu("command") }) if runtime.GOOS == "windows" { menu.Add("external-command", "c", "copy %~f to %~D2 ", func() { g.Shell("robocopy /e %~f %~D2") }, "m", "move %~f to %~D2 ", func() { g.Shell("move /-y %~f %~D2") }, "d", "del /s %~m ", func() { g.Shell("del /s %~m") }, "D", "rd /s /q %~m ", func() { g.Shell("rd /s /q %~m") }, "k", "make directory ", func() { g.Shell("mkdir ") }, "n", "create newfile ", func() { g.Shell("copy nul ") }, "r", "move (rename) %f ", func() { g.Shell("move /-y %~f ./") }, "w", "where . * ", func() { g.Shell("where . *") }, ) } else { menu.Add("external-command", "c", "copy %m to %D2 ", func() { g.Shell("cp -vai %m %D2") }, "m", "move %m to %D2 ", func() { g.Shell("mv -vi %m %D2") }, "D", "remove %m files ", func() { g.Shell("rm -vR %m") }, "k", "make directory ", func() { g.Shell("mkdir -vp ./") }, "n", "create newfile ", func() { g.Shell("touch ./") }, "T", "time copy %f to %m", func() { g.Shell("touch -r %f %m") }, "M", "change mode %m ", func() { g.Shell("chmod 644 %m", -3) }, "r", "move (rename) %f ", func() { g.Shell("mv -vi %f " + g.File().Name()) }, "R", "bulk rename %m ", func() { g.Shell(`rename -v "s///" %m`, -6) }, "f", "find . -name ", func() { g.Shell(`find . -name "*"`, -1) }, "A", "archives menu ", func() { g.Menu("archive") }, ) } g.AddKeymap("X", func() { g.Menu("external-command") }) menu.Add("archive", "z", "zip ", func() { g.Shell(`zip -roD %x.zip %m`, -7) }, "t", "tar ", func() { g.Shell(`tar cvf %x.tar %m`, -7) }, "g", "tar.gz ", func() { g.Shell(`tar cvfz %x.tgz %m`, -7) }, "b", "tar.bz2 ", func() { g.Shell(`tar cvfj %x.bz2 %m`, -7) }, "x", "tar.xz ", func() { g.Shell(`tar cvfJ %x.txz %m`, -7) }, "r", "rar ", func() { g.Shell(`rar u %x.rar %m`, -7) }, "Z", "extract zip for %m", func() { g.Shell(`for i in %m; do unzip "$i" -d ./; done`, -6) }, "T", "extract tar for %m", func() { g.Shell(`for i in %m; do tar xvf "$i" -C ./; done`, -6) }, "G", "extract tgz for %m", func() { g.Shell(`for i in %m; do tar xvfz "$i" -C ./; done`, -6) }, "B", "extract bz2 for %m", func() { g.Shell(`for i in %m; do tar xvfj "$i" -C ./; done`, -6) }, "X", "extract txz for %m", func() { g.Shell(`for i in %m; do tar xvfJ "$i" -C ./; done`, -6) }, "R", "extract rar for %m", func() { g.Shell(`for i in %m; do unrar x "$i" -C ./; done`, -6) }, "1", "find . *.zip extract", func() { g.Shell(`find . -name "*.zip" -type f -prune -print0 | xargs -n1 -0 unzip -d ./`) }, "2", "find . *.tar extract", func() { g.Shell(`find . -name "*.tar" -type f -prune -print0 | xargs -n1 -0 tar xvf -C ./`) }, "3", "find . *.tgz extract", func() { g.Shell(`find . -name "*.tgz" -type f -prune -print0 | xargs -n1 -0 tar xvfz -C ./`) }, "4", "find . *.bz2 extract", func() { g.Shell(`find . -name "*.bz2" -type f -prune -print0 | xargs -n1 -0 tar xvfj -C ./`) }, "5", "find . *.txz extract", func() { g.Shell(`find . -name "*.txz" -type f -prune -print0 | xargs -n1 -0 tar xvfJ -C ./`) }, "6", "find . *.rar extract", func() { g.Shell(`find . -name "*.rar" -type f -prune -print0 | xargs -n1 -0 unrar x -C ./`) }, ) menu.Add("bookmark", "t", "~/Desktop ", func() { g.Dir().Chdir("~/Desktop") }, "c", "~/Documents", func() { g.Dir().Chdir("~/Documents") }, "d", "~/Downloads", func() { g.Dir().Chdir("~/Downloads") }, "m", "~/Music ", func() { g.Dir().Chdir("~/Music") }, "p", "~/Pictures ", func() { g.Dir().Chdir("~/Pictures") }, "v", "~/Videos ", func() { g.Dir().Chdir("~/Videos") }, ) if runtime.GOOS == "windows" { menu.Add("bookmark", "C", "C:/", func() { g.Dir().Chdir("C:/") }, "D", "D:/", func() { g.Dir().Chdir("D:/") }, "E", "E:/", func() { g.Dir().Chdir("E:/") }, ) } else { menu.Add("bookmark", "e", "/etc ", func() { g.Dir().Chdir("/etc") }, "u", "/usr ", func() { g.Dir().Chdir("/usr") }, "x", "/media ", func() { g.Dir().Chdir("/media") }, ) } g.AddKeymap("b", func() { g.Menu("bookmark") }) menu.Add("editor", "c", "vscode ", func() { g.Spawn("code %f %&") }, "e", "emacs client ", func() { g.Spawn("emacsclient -n %f %&") }, "v", "vim ", func() { g.Spawn("vim %f") }, ) g.AddKeymap("e", func() { g.Menu("editor") }) menu.Add("image", "x", "default ", func() { g.Spawn(opener) }, "e", "eog ", func() { g.Spawn("eog %f %&") }, "g", "gimp ", func() { g.Spawn("gimp %m %&") }, ) menu.Add("media", "x", "default ", func() { g.Spawn(opener) }, "m", "mpv ", func() { g.Spawn("mpv %f") }, "v", "vlc ", func() { g.Spawn("vlc %f %&") }, ) var associate widget.Keymap if runtime.GOOS == "windows" { associate = widget.Keymap{ ".dir": func() { g.Dir().EnterDir() }, ".go": func() { g.Shell("go run %~f") }, ".py": func() { g.Shell("python %~f") }, ".rb": func() { g.Shell("ruby %~f") }, ".js": func() { g.Shell("node %~f") }, } } else { associate = widget.Keymap{ ".dir": func() { g.Dir().EnterDir() }, ".exec": func() { g.Shell(" ./" + g.File().Name()) }, ".zip": func() { g.Shell("unzip %f -d %D") }, ".tar": func() { g.Shell("tar xvf %f -C %D") }, ".gz": func() { g.Shell("tar xvfz %f -C %D") }, ".tgz": func() { g.Shell("tar xvfz %f -C %D") }, ".bz2": func() { g.Shell("tar xvfj %f -C %D") }, ".xz": func() { g.Shell("tar xvfJ %f -C %D") }, ".txz": func() { g.Shell("tar xvfJ %f -C %D") }, ".rar": func() { g.Shell("unrar x %f -C %D") }, ".go": func() { g.Shell("go run %f") }, ".py": func() { g.Shell("python %f") }, ".rb": func() { g.Shell("ruby %f") }, ".js": func() { g.Shell("node %f") }, ".jpg": func() { g.Menu("image") }, ".jpeg": func() { g.Menu("image") }, ".gif": func() { g.Menu("image") }, ".png": func() { g.Menu("image") }, ".bmp": func() { g.Menu("image") }, ".avi": func() { g.Menu("media") }, ".mp4": func() { g.Menu("media") }, ".mkv": func() { g.Menu("media") }, ".wmv": func() { g.Menu("media") }, ".flv": func() { g.Menu("media") }, ".mp3": func() { g.Menu("media") }, ".flac": func() { g.Menu("media") }, ".tta": func() { g.Menu("media") }, } } g.MergeExtmap(widget.Extmap{ "C-m": associate, "o": associate, }) } // Widget keymap functions. func filerKeymap(g *app.Goful) widget.Keymap { return widget.Keymap{ "M-C-o": func() { g.CreateWorkspace() }, "M-C-w": func() { g.CloseWorkspace() }, "M-f": func() { g.MoveWorkspace(1) }, "M-b": func() { g.MoveWorkspace(-1) }, "C-o": func() { g.Workspace().CreateDir() }, "C-w": func() { g.Workspace().CloseDir() }, "C-l": func() { g.Workspace().ReloadAll() }, "C-f": func() { g.Workspace().MoveFocus(1) }, "C-b": func() { g.Workspace().MoveFocus(-1) }, "right": func() { g.Workspace().MoveFocus(1) }, "left": func() { g.Workspace().MoveFocus(-1) }, "C-i": func() { g.Workspace().MoveFocus(1) }, "l": func() { g.Workspace().MoveFocus(1) }, "h": func() { g.Workspace().MoveFocus(-1) }, "F": func() { g.Workspace().SwapNextDir() }, "B": func() { g.Workspace().SwapPrevDir() }, "w": func() { g.Workspace().ChdirNeighbor() }, "C-h": func() { g.Dir().Chdir("..") }, "backspace": func() { g.Dir().Chdir("..") }, "u": func() { g.Dir().Chdir("..") }, "~": func() { g.Dir().Chdir("~") }, "\\": func() { g.Dir().Chdir("/") }, "C-n": func() { g.Dir().MoveCursor(1) }, "C-p": func() { g.Dir().MoveCursor(-1) }, "down": func() { g.Dir().MoveCursor(1) }, "up": func() { g.Dir().MoveCursor(-1) }, "j": func() { g.Dir().MoveCursor(1) }, "k": func() { g.Dir().MoveCursor(-1) }, "C-d": func() { g.Dir().MoveCursor(5) }, "C-u": func() { g.Dir().MoveCursor(-5) }, "C-a": func() { g.Dir().MoveTop() }, "C-e": func() { g.Dir().MoveBottom() }, "home": func() { g.Dir().MoveTop() }, "end": func() { g.Dir().MoveBottom() }, "^": func() { g.Dir().MoveTop() }, "$": func() { g.Dir().MoveBottom() }, "M-n": func() { g.Dir().Scroll(1) }, "M-p": func() { g.Dir().Scroll(-1) }, "C-v": func() { g.Dir().PageDown() }, "M-v": func() { g.Dir().PageUp() }, "pgdn": func() { g.Dir().PageDown() }, "pgup": func() { g.Dir().PageUp() }, " ": func() { g.Dir().ToggleMark() }, "M-=": func() { g.Dir().InvertMark() }, "C-g": func() { g.Dir().Reset() }, "C-[": func() { g.Dir().Reset() }, // C-[ means ESC "f": func() { g.Dir().Finder() }, "/": func() { g.Dir().Finder() }, "q": func() { g.Quit() }, "Q": func() { g.Quit() }, ";": func() { g.Shell("") }, ":": func() { g.ShellSuspend("") }, "M-W": func() { g.ChangeWorkspaceTitle() }, "n": func() { g.Touch() }, "K": func() { g.Mkdir() }, "c": func() { g.Copy() }, "m": func() { g.Move() }, "r": func() { g.Rename() }, "R": func() { g.BulkRename() }, "D": func() { g.Remove() }, "d": func() { g.Chdir() }, "g": func() { g.Glob() }, "G": func() { g.Globdir() }, } } func finderKeymap(w *filer.Finder) widget.Keymap { return widget.Keymap{ "C-h": func() { w.DeleteBackwardChar() }, "backspace": func() { w.DeleteBackwardChar() }, "M-p": func() { w.MoveHistory(1) }, "M-n": func() { w.MoveHistory(-1) }, "C-g": func() { w.Exit() }, "C-[": func() { w.Exit() }, } } func cmdlineKeymap(w *cmdline.Cmdline) widget.Keymap { return widget.Keymap{ "C-a": func() { w.MoveTop() }, "C-e": func() { w.MoveBottom() }, "C-f": func() { w.ForwardChar() }, "C-b": func() { w.BackwardChar() }, "right": func() { w.ForwardChar() }, "left": func() { w.BackwardChar() }, "M-f": func() { w.ForwardWord() }, "M-b": func() { w.BackwardWord() }, "C-d": func() { w.DeleteChar() }, "delete": func() { w.DeleteChar() }, "C-h": func() { w.DeleteBackwardChar() }, "backspace": func() { w.DeleteBackwardChar() }, "M-d": func() { w.DeleteForwardWord() }, "M-h": func() { w.DeleteBackwardWord() }, "C-k": func() { w.KillLine() }, "C-i": func() { w.StartCompletion() }, "C-m": func() { w.Run() }, "C-g": func() { w.Exit() }, "C-[": func() { w.Exit() }, "C-n": func() { w.History.CursorDown() }, "C-p": func() { w.History.CursorUp() }, "down": func() { w.History.CursorDown() }, "up": func() { w.History.CursorUp() }, "C-v": func() { w.History.PageDown() }, "M-v": func() { w.History.PageUp() }, "pgdn": func() { w.History.PageDown() }, "pgup": func() { w.History.PageUp() }, "M-<": func() { w.History.MoveTop() }, "M->": func() { w.History.MoveBottom() }, "home": func() { w.History.MoveTop() }, "end": func() { w.History.MoveBottom() }, "M-n": func() { w.History.Scroll(1) }, "M-p": func() { w.History.Scroll(-1) }, "C-x": func() { w.History.Delete() }, } } func completionKeymap(w *cmdline.Completion) widget.Keymap { return widget.Keymap{ "C-n": func() { w.CursorDown() }, "C-p": func() { w.CursorUp() }, "down": func() { w.CursorDown() }, "up": func() { w.CursorUp() }, "C-f": func() { w.CursorToRight() }, "C-b": func() { w.CursorToLeft() }, "right": func() { w.CursorToRight() }, "left": func() { w.CursorToLeft() }, "C-i": func() { w.CursorToRight() }, "C-v": func() { w.PageDown() }, "M-v": func() { w.PageUp() }, "pgdn": func() { w.PageDown() }, "pgup": func() { w.PageUp() }, "M-<": func() { w.MoveTop() }, "M->": func() { w.MoveBottom() }, "home": func() { w.MoveTop() }, "end": func() { w.MoveBottom() }, "M-n": func() { w.Scroll(1) }, "M-p": func() { w.Scroll(-1) }, "C-m": func() { w.InsertCompletion() }, "C-g": func() { w.Exit() }, "C-[": func() { w.Exit() }, } } func menuKeymap(w *menu.Menu) widget.Keymap { return widget.Keymap{ "C-n": func() { w.MoveCursor(1) }, "C-p": func() { w.MoveCursor(-1) }, "down": func() { w.MoveCursor(1) }, "up": func() { w.MoveCursor(-1) }, "C-v": func() { w.PageDown() }, "M-v": func() { w.PageUp() }, "M->": func() { w.MoveBottom() }, "M-<": func() { w.MoveTop() }, "C-m": func() { w.Exec() }, "C-g": func() { w.Exit() }, "C-[": func() { w.Exit() }, } }