Day 6: Manage your dotfiles easily with nistow
Today we will create a tool to manage our dotfiles easily.
Dotfiles layout
i3
`-- .config
`-- i3
`-- config
So we have here a directory named i3 in the very top indicates APP_NAME
and under it a tree of config paths. Here it means config
file is supposed to be linked under .config/i3/config
relative to destination directory
Home directory is the default destination.
What do we expect?
➜ ~ nistow --help
Stow 0.1.0
-h | --help : show help
-v | --version : show version
--verbose : verbose messages
-s | --simulate : simulate stow operation
-f | --force : override old links
-a | --app : application path to stow
-d | --dest : destination to stow to
--simulate
flag used to simulate on the filesystem without actual linking--app
application directory that's compatible with the dotfiles layoud described above.--dest
destination to symlink files under, defaults to home dir.
nistow --app=/home/striky/wspace/dotfiles/localdir --dest=/tmp/tmpconf --verbose
Implementation
proc writeHelp() =
echo """
Stow 0.1.0 (Manage your dotfiles easily)
Allowed arguments:
-h | --help : show help
-v | --version : show version
--verbose : verbose messages
-s | --simulate : simulate stow operation
-f | --force : override old links
-a | --app : application path to stow
-d | --dest : destination to stow to
"""
writeHelp
is a simple proc to write help string to the stdout
proc writeVersion() =
echo "Stow version 0.1.0"
To write version
proc cli*() =
Entry point for out commandline application
var
simulate, verbose, force: bool = false
app, dest: string = ""
Variables represents various options we allow in the application.
if paramCount() == 0:
writeHelp()
quit(0)
If no arguments passed we will write the help string and exit
or quit
according to nim with exit status
0
for kind, key, val in getopt():
case kind
of cmdLongOption, cmdShortOption:
case key
of "help", "h":
writeHelp()
quit()
of "version", "v":
writeVersion()
quit()
of "simulate", "s": simulate = true
of "verbose": verbose = true
of "force", "f": force = true
of "app", "a": app = val
of "dest", "d": dest = val
else:
discard
else:
discard
Here we parse the commandline string using getopt
.
for kind, key, val in getopt():
case kind
of cmdLongOption, cmdShortOption:
So for --app=/home/striky/dotfiles/i3 -f
kind for --app
is cmdLongOption
and for -f
is cmdShortOption
key for --app
is app
and for -f
is f
val for --app
is /home/striky/dotfiles/i3
val for -f
we set to true
in our parsing, because it's mainly like a switch boolean
if it exists it means we want it set to true.
if dest.isNilOrEmpty():
dest = getHomeDir()
Here we set default dest
to homeDir
if app.isNilOrEmpty():
echo "Make sure to provide --app flags"
quit(1)
Here we exit with error exit status
1 if app isn't set.
try:
stow(getLinkableFiles(appPath=app, dest=dest), simulate=simulate, verbose=verbose, force=force)
except ValueError:
echo "Error happened: " & getCurrentExceptionMsg()
Here we try to stow all the linkable files in app
dir to dest
dir and pass all the options we collected from the command line arguments simulate
, verbose
, force
, and wrapped around try/except
to show error to the user
when isMainModule:
cli()
invoke our entry point cli
if this module is the main module.
OK! back to stow and getLinkableFiles
We start with getLinkableFiles
. Remember the dotfiles hierarchy?
# appPath: application's dotfiles directory
# we expect dir to have the hierarchy.
# i3
# `-- .config
# `-- i3
# `-- config
We want to get all the files in there with full path and the link file to each one will be exactly the same except for the appPath
name will be changed to dest
path
[/home/striky/wspace/dotfiles/i3]/.config/i3/config -> [/home/striky]/.config/i3/config
__________________appPath________ _____dest____
type
LinkInfo = tuple[original:string, dest:string]
Simple type to represent the original path and where to symlink to
proc getLinkableFiles*(appPath: string, dest: string=expandTilde("~")): seq[LinkInfo] =
# collects the linkable files in a certain app.
# appPath: application's dotfiles directory
# we expect dir to have the hierarchy.
# i3
# `-- .config
# `-- i3
# `-- config
# dest: destination of the link files : default is the home of user.
getLinkableFiles
is a proc takes appPath
and dest
and returns a seq
of LinkInfo contains this transformation for each file.
[/home/striky/wspace/dotfiles/i3]/A_FILE_PATH -> [/home/striky]A_FILE_PATH
__________________apppath________ _____dest____
var appPath = expandTilde(appPath)
if not dirExists(appPath):
raise newException(ValueError, fmt("App path {appPath} doesn't exist."))
var linkables = newSeq[LinkInfo]()
for filepath in walkDirRec(appPath, yieldFilter={pcFile}):
let linkpath = filepath.replace(appPath, dest)
var linkInfo : LinkInfo = (original:filepath, dest:linkpath)
linkables.add(linkInfo)
return linkables
Here, we walk over the appPath
dir using walkDirRec
and specify in yieldFilter
argument that we're interested in pcFile
"file path component", just call it entries of type regular file.
proc stow(linkables: seq[LinkInfo], simulate: bool=true, verbose: bool=true, force: bool=false) =
# Creates symoblic links and related directories
# linkables is a list of tuples (filepath, linkpath) : List[Tuple[file_path, link_path]]
# simulate does simulation with no effect on the filesystem: bool
# verbose shows log messages: bool
for linkinfo in linkables:
let (filepath, linkpath) = linkinfo
if verbose:
echo(fmt("Will link {filepath} -> {linkpath}"))
if not simulate:
createDir(parentDir(linkpath))
if not fileExists(linkpath):
createSymlink(filepath, linkpath)
else:
if force:
removeFile(linkpath)
createSymlink(filepath, linkpath)
else:
if verbose:
echo(fmt("Skipping linking {filepath} -> {linkpath}"))
stow is pretty easy procedure, it takes in a list of LinksInfo
that has all the information (original filename and destination symlink) and does the symlinking based on if it's not a simulation and prints the messages if verbose is set to true
Feel free to send improvements to this tutorial or nistow :)
Complete source code available here https://github.com/xmonader/nistow