diff options
-rw-r--r-- | Makefile | 23 | ||||
-rw-r--r-- | README.md | 88 | ||||
-rw-r--r-- | main.go | 379 | ||||
-rw-r--r-- | wrapper.go | 120 |
4 files changed, 610 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ea9562c --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +ifeq ($(PREFIX),) + PREFIX := /usr/local +endif + +ifeq ($(BINDIR),) + BINDIR := /bin +endif + +CMD=ws_internal + +make: ws_internal + +ws_internal: main.go wrapper.go + go build -o $@ $^ + +install: + mkdir -p $(DESTDIR)$(PREFIX)$(BINDIR)/ + install -m 755 $(CMD) $(DESTDIR)$(PREFIX)$(BINDIR)/ + +uninstall: + rm -rf $(DESTDIR)$(PREFIX)$(BINDIR)/$(CMD) + +.PHONY: install uninstall make diff --git a/README.md b/README.md new file mode 100644 index 0000000..3f88c9b --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +Temporary Workspaces Manager (ws) +================================= + +Introduction: +------------- + +`ws` is a lightweight bash program designed to create and manage temporary workspaces. It proves useful in command-line intensive workflows, allowing users to quickly create temporary directories for tasks like prototyping programs, transcoding audio, or downloading files. This program was developed as a means to sharpen my skills in the Go programming language. It incorporates a shell wrapper to access shell built-ins within the current session. + +Installation: +------------- + +1. Run the following commands +```bash +make +sudo make install +mkdir ~/ws +``` + +2. Add the following line to your .zshrc or .bashrc file: +```bash +eval "$(ws_internal activate)" +``` + +Usage: +----- + +```text +Usage: ws [command] + +ws is a program for managing temporary workspaces. + +Commands: + list|ls List all available workspaces + new Create a new workspace and change into it + print_current_workspace|pcw + Print the current workspace path + recent|rec Change to the most recently used workspace + next|n Change to the next workspace in the history + prev|p Change to the previous workspace in the history + remove|rm Remove the current workspace and return to the previous one + return|ret Return to the previous workspace + usage Show this usage information +``` + +Examples: +--------- + +Create a new workspace and change into it: + +```text +[user@ubuntu ~] ws new +[user@ubuntu 2023-06-25_07:35:20.2685626823] +``` +Return to the previous location with `ret`: + +```text +[user@ubuntu 2023-06-25_07:35:20.2685626823] ws ret +[user@ubuntu ~] +``` + +List workspaces with `ls`: + +```text +[user@ubuntu ~] ws ls +1 2023-06-25_07:29:02.3918850963 0 days 0 hours 6 min 58 secs +2 2023-06-25_07:35:20.2685626823 0 days 0 hours 0 min 41 secs +``` + +Change into the most recently accessed workspace with `ws`: + +```text +[user@ubuntu ~] ws +[user@ubuntu 2023-06-25_07:35:20.2685626823] +``` + +Move back one workspace with `prev` or `p`: + +```text +[user@ubuntu 2023-06-25_07:35:20.2685626823] ws p +[user@ubuntu 2023-06-25_07:29:02.3918850963] +``` + +Move forward one workspace with `next` or `n`: + +```text +[user@ubuntu 2023-06-25_07:29:02.3918850963] ws n +[user@ubuntu 2023-06-25_07:35:20.2685626823] +``` @@ -0,0 +1,379 @@ +package main + +import ( + "errors" + "fmt" + "log" + "os" + "path" + "path/filepath" + "sort" + "strings" + "time" +) + +/********************************************* + * Workspace Root + *********************************************/ + +type workspaceRoot struct { + path string +}; + +func systemWorkspaceRoot() workspaceRoot { + const WORKSPACE_DIR = "ws" + + home, err := os.UserHomeDir() + + if err != nil { + log.Fatalf("The user home directory is not defined: %v", err) + } + + workspaceRootPath := filepath.Join(home, WORKSPACE_DIR); + + _, err = os.Stat(workspaceRootPath) + + if os.IsNotExist(err) { + if err := os.Mkdir(workspaceRootPath, 0755); err != nil { + log.Fatalf("Failed to create directory: %s", workspaceRootPath); + } + } + + return workspaceRoot { workspaceRootPath } +} + +type workspaceDirs []*workspaceDir + +func (wsRoot *workspaceRoot) readWorkspaces() workspaceDirs { + dirs, err := os.ReadDir(wsRoot.path) + + if err != nil { + log.Fatalf("Failed to read workspaces in %s: %v", wsRoot.path, err); + } + + workspaces := make([]*workspaceDir, 0) + + for _, dir := range dirs { + if !dir.Type().IsDir() { + continue + } + + wsDir, err := newWorkspaceFromName(wsRoot, dir.Name()) + + if err != nil { + log.Printf("Error reading directory in workspace %s: %s: %v", wsRoot.path, dir.Name(), err); + continue + } + + workspaces = append(workspaces, wsDir); + } + + return workspaces +} + +func (dirs workspaceDirs) findDirByName(name string) *workspaceDir { + birthTime, nonce, err := parseWorkspaceName(name) + + if err != nil { + log.Fatalf("Cannot find directory %s, error parsing name", name); + } + + var dir *workspaceDir = nil + + for _, dir = range dirs { + if dir.birthTime.Equal(*birthTime) && dir.nonce == *nonce { + break + } + } + + return dir +} + +/********************************************* + * Workspace Directory Object + *********************************************/ + +type workspaceDir struct { + birthTime time.Time + nonce string + accessTime time.Time + parent *workspaceRoot +} + +var MisformattedWorkspaceError = errors.New("Misformatted workspace name error") + +func formatWorkspaceName(birthTime time.Time, nonce string) string { + dateTime := birthTime.UTC().Format(time.DateTime) + dateTime = strings.ReplaceAll(dateTime, " ", "_") + return dateTime + "." + nonce +} + +func parseWorkspaceName(name string) (birthTime *time.Time, nonce *string, err error) { + parts := strings.Split(name, ".") + + if len(parts) != 2 { + return nil, nil, MisformattedWorkspaceError + } + + dateTime := strings.ReplaceAll(parts[0], "_", " ") + + timePart, err := time.ParseInLocation(time.DateTime, dateTime, time.UTC) + noncePart := parts[1] + + if err != nil { + return nil, nil, MisformattedWorkspaceError + } + + return &timePart, &noncePart, nil +} + +func newWorkspaceFromName(parent *workspaceRoot, name string) (dir *workspaceDir, err error) { + dir = nil + + birthTime, nonce, err := parseWorkspaceName(name) + if err != nil { + return + } + + st, err := os.Stat(path.Join(parent.path, name)) + if err != nil { + return + } + + dir = &workspaceDir { + *birthTime, + *nonce, + st.ModTime(), + parent, + } + + return +} + +func createWorkspaceDir(ws *workspaceRoot) workspaceDir { + birthTime := time.Now() + namePattern := formatWorkspaceName(birthTime, "*") + + dir, err := os.MkdirTemp(ws.path, namePattern) + if err != nil { + log.Fatalf("Failed to create new workspace directory: %v", err); + } + + dir = path.Base(dir) + + _, nonce, err := parseWorkspaceName(dir) + + if err != nil { + log.Fatalf("Failed to parse the workspace name: %v\n", err); + } + + return workspaceDir { + birthTime, + *nonce, + birthTime, + ws, + }; +} + +func (dir *workspaceDir) name() string { + return formatWorkspaceName(dir.birthTime, dir.nonce) +} + +func (dir *workspaceDir) path() string { + return path.Join(dir.parent.path, dir.name()) +} + +func (dir *workspaceDir) timeSinceLastAccess() time.Duration { + return time.Since(dir.accessTime) +} + +/********************************************* + * Commands Helpers + *********************************************/ + +const ( + secondsInSec = 1 + minsInSec = 60 * secondsInSec + hourInSec = 60 * minsInSec + dayInSec = 24 * hourInSec +) + +func formatTimestamp(duration time.Duration) string { + secs := int64(duration.Seconds()) + days := secs / dayInSec + secs %= dayInSec + hours := secs / hourInSec + secs %= hourInSec + mins := secs / minsInSec + secs %= minsInSec + + return fmt.Sprintf("%d days %d hours %d min %d secs", days, hours, mins, secs) +} + +func readRecencySortedWorkspaces() (*workspaceRoot, workspaceDirs) { + wsRoot := systemWorkspaceRoot() + workspaces := wsRoot.readWorkspaces() + + sort.Slice(workspaces, func(i, j int) bool { + return workspaces[i].accessTime.Compare(workspaces[j].accessTime) < 0 + }) + + return &wsRoot, workspaces +} + +func currentWorkspace(wsRoot *workspaceRoot, workspaces workspaceDirs) int { + + cwd, err := os.Getwd() + + if err != nil { + log.Fatalf("Failed to obtain the working directory: %s\n", err); + } + + if !strings.HasPrefix(cwd, wsRoot.path) { + log.Fatalf("The current working directory is not in a workspace\n") + } + + dirName := path.Base(cwd) + + cur_idx := 0 + for ; cur_idx < len(workspaces); cur_idx++ { + dir := workspaces[cur_idx] + if dir.name() == dirName { + break + } + } + + if cur_idx == len(workspaces) { + log.Fatalf("Failed to find %s\n", dirName) + } + + return cur_idx +} + +/********************************************* + * Commands + *********************************************/ + +func createNewWorkspace() { + ws := systemWorkspaceRoot() + workspace := createWorkspaceDir(&ws) + path := workspace.path() + + if err := os.Chdir(path); err != nil { + log.Fatalf("Failed to change into directory: %v\n", err) + } + + fmt.Println(path) +} + +func listWorkspaces() { + _, workspaces := readRecencySortedWorkspaces() + + for i, ws := range workspaces { + delta := ws.timeSinceLastAccess() + + fmt.Println(i + 1, ws.name(), formatTimestamp(delta)) + } +} + +func printRecentWorkspace() { + _, workspaces := readRecencySortedWorkspaces() + + if len(workspaces) == 0 { + log.Fatalln("There are currently no workspaces. Create a workspace with `ws new`.") + } + + fmt.Println(workspaces[len(workspaces) - 1].path()) +} + +type printWorkspaceSubcmd int + +const ( + PrevWs printWorkspaceSubcmd = iota + NextWs + CurrWs +) + +func printWorkspace(subcmd printWorkspaceSubcmd) { + wsRoot, workspaces := readRecencySortedWorkspaces() + + idx := currentWorkspace(wsRoot, workspaces) + + switch subcmd { + case PrevWs: + if idx <= 0 { + log.Fatalln("The first workspace has been reached") + } + + idx-- + case NextWs: + if idx + 1 >= len(workspaces) { + log.Fatalln("The current workspace is the newest workspace") + } + + idx++ + } + + fmt.Println(workspaces[idx].path()) +} + +func printWorkspaceRoot() { + wsRoot := systemWorkspaceRoot() + fmt.Println(wsRoot.path) +} + +func fatalPrintUsage() { +fmt.Printf(` + ws internal + + ws_internal create_new_workspace + ws_internal list_workspaces + ws_internal print_recent_workspace + ws_internal print_current_workspace + ws_internal print_next_workspace + ws_internal print_prev_workspace + ws_internal print_workspace_root + ws_internal activate +`) + os.Exit(1) +} + +func main() { + + if len(os.Args) < 1 { + os.Exit(127) + } + + prog := os.Args[0] + + log.SetPrefix(prog + ": ") + log.SetFlags(0) + + if len(os.Args) < 2 { + fatalPrintUsage() + } + + cmd := os.Args[1] + + switch cmd { + case "create_new_workspace": + createNewWorkspace() + case "list_workspaces": + listWorkspaces() + case "print_recent_workspace": + printRecentWorkspace() + case "print_current_workspace": + printWorkspace(CurrWs) + case "print_next_workspace": + printWorkspace(NextWs) + case "print_prev_workspace": + printWorkspace(PrevWs) + case "print_workspace_root": + printWorkspaceRoot() + case "activate": + fmt.Print(shellWrapper) + default: + fatalPrintUsage() + } + +} diff --git a/wrapper.go b/wrapper.go new file mode 100644 index 0000000..45c04f4 --- /dev/null +++ b/wrapper.go @@ -0,0 +1,120 @@ +package main + +const shellWrapper=` +# Declare constant for the executable + +# Print error message +__ws_err() { + if [[ $# -eq 0 ]]; then + echo -n "ws: " 1>&2 + cat <&0 1>&2 + elif [[ $# -eq 1 ]]; then + echo "ws: $1" 1>&2 + fi +} + +# Execute internal workspace command +__ws_internal() { + __WS_INTERNAL_EXE="ws_internal" + ${__WS_INTERNAL_EXE} "$@" + return $? +} + +# Record the current working directory before changing to workspace +declare WS_PREVIOUS_LOC="" +__ws_push_wd() { + WS_PREVIOUS_LOC="$(pwd)" +} + +# Return to the previously recorded directory +__ws_pop_wd() { + if [[ -z "${WS_PREVIOUS_LOC}" ]]; then + ws_err "No working directory was recorded prior to entering the workspace" + return 1 + fi + cd "${WS_PREVIOUS_LOC}" + WS_PREVIOUS_LOC="" + return 0 +} + +# Workspace usage instruction +__ws_usage() { + __ws_err <<_EOF +Usage: ws [command] + +ws is a program for managing temporary workspaces. + +Commands: + list|ls List all available workspaces + new Create a new workspace and change into it + print_current_workspace|pcw + Print the current workspace path + recent|rec Change to the most recently used workspace + next|n Change to the next workspace in the history + prev|p Change to the previous workspace in the history + remove|rm Remove the current workspace and return to the previous one + return|ret Return to the previous workspace + usage Show this usage information +_EOF +} + +# Workspace function +ws() { + local cmd="${1:-recent}" + + case "$cmd" in + "list"|"ls") + __ws_internal list_workspaces + ;; + "new") + __ws_push_wd + local ws_path="$(__ws_internal create_new_workspace)" + echo "${ws_path}" + [[ ! $? -eq 0 ]] && return $? + cd "${ws_path}" + ;; + "print_current_workspace"|"pcw") + __ws_internal print_current_workspace + [[ ! $? -eq 0 ]] && return $? + ;; + "recent"|"rec"|"next"|"n"|"prev"|"p") + + local internal_cmd="" + if [[ "$cmd" == "rec" || "$cmd" == "recent" ]]; then + __ws_push_wd + internal_cmd="recent" + elif [[ "$cmd" == "next" || "$cmd" == "n" ]]; then + internal_cmd="next" + elif [[ "$cmd" == "prev" || "$cmd" == "p" ]]; then + internal_cmd="prev" + fi + + local ws_path="$(__ws_internal "print_${internal_cmd}_workspace")" + [[ ! $? -eq 0 ]] && return $? + cd "${ws_path}" + + ;; + "remove"|"rm") + local ws_path="$(__ws_internal print_recent_workspace)" + rm -rf "${ws_path}" + if [[ -n "${WS_PREVIOUS_LOC}" ]]; then + __ws_pop_wd || return $? + else + local ws_path="$(__ws_internal print_workspace_root)" + [[ ! $? -eq 0 ]] && return $? + cd "${ws_path}" + fi + ;; + "return"|"ret") + __ws_pop_wd + ;; + "usage") + __ws_usage + ;; + *) + __ws_usage + return 1 + ;; + esac +} +` |