aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile23
-rw-r--r--README.md88
-rw-r--r--main.go379
-rw-r--r--wrapper.go120
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]
+```
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..3dfe15c
--- /dev/null
+++ b/main.go
@@ -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
+}
+`