diff --git a/.gitignore b/.gitignore index 49efd49..e013ae3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,113 @@ -*.swp -*.o -promptoglyph.tar.gz -promptoglyph-path -promptoglyph-vcs +*.swp +*.o +build/ +.DS_Store +promptoglyph.tar.gz +promptoglyph-path +promptoglyph-vcs + +# Created by https://www.gitignore.io/api/windows,sublimetext,osx,linux,d + +### Windows ### +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + + +### SublimeText ### +# cache files for sublime text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# workspace files are user-specific +*.sublime-workspace + +*.sublime-project + +# project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using SublimeText +# *.sublime-project + +# sftp configuration file +sftp-config.json + + +### OSX ### +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + + +### D ### +# Compiled Object files +*.o +*.obj + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Compiled Static libraries +*.a +*.lib + +# Executables +*.exe + +# DUB +.dub +docs.json +__dummy.html + diff --git a/Makefile b/Makefile index a4e810e..0fb76dd 100644 --- a/Makefile +++ b/Makefile @@ -18,11 +18,11 @@ package: clean release gzip -f9 promptoglyph.tar rm -r promptoglyph -$(BUILD_DIR)/promptoglyph-path: promptoglyph-path.d help.d +$(BUILD_DIR)/promptoglyph-path: promptoglyph-path.d systempath.d help.d @mkdir -p $(BUILD_DIR) $(DC) $(DFLAGS) -of$@ $^ -$(BUILD_DIR)/promptoglyph-vcs: promptoglyph-vcs.d help.d vcs.d time.d color.d git.d +$(BUILD_DIR)/promptoglyph-vcs: promptoglyph-vcs.d systempath.d help.d vcs.d time.d color.d git.d @mkdir -p $(BUILD_DIR) $(DC) $(DFLAGS) -of$@ $^ diff --git a/README.md b/README.md index 3dd897f..7a53355 100644 --- a/README.md +++ b/README.md @@ -40,19 +40,31 @@ but Probably Work™ since they only depend on vanilla C libraries Alternatively, building form source is simple. -## How do I build it? +### Windows builds +Please build from source for now. -Grab a [D compiler](http://dlang.org/download.html) and run `make release`. +## How do I build it? +Grab a [D compiler](http://dlang.org/download.html) and follow the instructions for your platform below. That's all. There are no dependencies. promptoglyph will be added as a [Dub](http://code.dlang.org) package soon-ish. + +### Posix-compliant systems (like Linux, Mac OS X, etc.) +Just run `make release` and you should be set. + +### Windows +Run `build.bat -r` to build the release version. +Run `build.bat -d` to build the debug version. + + ## How do I use it for my prompt? Just invoke the programs with the desired options (see their `--help` info) in your prompt expression. +### Posix-compliant systems (like Linux, Mac OS X, etc.) In Zsh (for the prompt shown in the demo): ```shell @@ -67,6 +79,38 @@ In Bash, PS1="\$(promptoglyph-path) \$(promptoglyph-vcs --bash) % " ``` +### Windows +For the default *cmd.exe* you are out of luck, as it does not support custom program execution and only has a limited number of available options. + +Powershell on the other hand can use promptoglyph to enhance its prompt. +You will need to configure your [Powershell $profile](https://technet.microsoft.com/en-us/library/hh847857.aspx) to set your prompt accordingly. +Example Profile.ps1 (or Microsoft.PowerShell_profile.ps1) file: + +```powershell +# may have to set this first: Set-ExecutionPolicy -ExecutionPolicy Unrestricted +Set-ExecutionPolicy -ExecutionPolicy Unrestricted + +# . $profile # ==> reload powershell config + +function prompt +{ + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [Security.Principal.WindowsPrincipal] $identity + # promptoglyph.exe must be available in the %PATH% + $git = & "promptoglyph-vcs.exe" 2>&1 | Out-String + + $(if (test-path variable:/PSDebugContext) { '[DBG]: ' } + + elseif($principal.IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) + { "[ADMIN]: " } + + else { '' }) + 'PS ' + $(Get-Location) + ' ' + $git + $(if ($nestedpromptlevel -ge 1) { '>>' }) + '> ' +} +``` + +For *Cygwin* you can use the compiled exe files and copy them into /usr/local/bin for example and then use them in your ~/.bashrc or ~/.profile configuration. Unfortunately colored output is not yet supported. Feel free to help improving the situation by digging into the code. + + ## It's 2015. Why are you generating a prompt with a compiled program? I was using Zsh with [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh), diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..bfbac67 --- /dev/null +++ b/build.bat @@ -0,0 +1,41 @@ +@echo off +SETLOCAL ENABLEEXTENSIONS + +REM ------------- +REM I could not figure out a way to get DigitalMars' make.exe to like +REM the Unix style Makefile. If there is a way to change the Makefile +REM to allow both make.exe (by DigitalMars, comes with the D installation) +REM and make on Linux/Mac OS X to use it, please let me know. +REM ------------- + +REM no spaces between var name and assignment! +SET ME=%~n0 +SET PARENT=%~dp0 +SET BUILD_DIR=%PARENT%\build +SET DC=dmd +SET DFLAGS=-wi -g + +IF /I "%1"=="/debug" GOTO LABEL_DEBUG +IF /I "%1"=="--debug" GOTO LABEL_DEBUG +IF /I "%1"=="-d" GOTO LABEL_DEBUG +IF /I "%1"=="/release" GOTO LABEL_RELEASE +IF /I "%1"=="--release" GOTO LABEL_RELEASE +IF /I "%1"=="-r" GOTO LABEL_RELEASE + +GOTO LABEL_AFTER_VARIABLES +:LABEL_DEBUG +SET DFLAGS=%DFLAGS% -debug +GOTO LABEL_AFTER_VARIABLES +:LABEL_RELEASE +SET DFLAGS=%DFLAGS% -O -release + +:LABEL_AFTER_VARIABLES + +rmdir /s /q %BUILD_DIR% +mkdir %BUILD_DIR% + +echo on + +%DC% %DFLAGS% -of%BUILD_DIR%\promptoglyph-vcs.exe promptoglyph-vcs.d systempath.d help.d vcs.d time.d color.d git.d +%DC% %DFLAGS% -of%BUILD_DIR%\promptoglyph-path.exe promptoglyph-path.d systempath.d help.d + diff --git a/color.d b/color.d index e4b0730..0a9061d 100644 --- a/color.d +++ b/color.d @@ -5,7 +5,8 @@ alias UseColor = Flag!"UseColor"; enum Escapes { none, bash, - zsh + zsh, + cmd } mixin(makeColorFunction("cyan", 36)); @@ -16,6 +17,23 @@ mixin(makeColorFunction("resetColor", 39)); private: +// Getting colored chars into the cmd.exe command line window is nearly impossible. :-( +// http://superuser.com/questions/427820/how-to-change-only-the-prompt-color-of-the-windows-command-line +// Quoting: +// +// Following the prompt of @Luke I finally get the solution. Anyone who is interested in this topic please hit the two links below: +// +// Color for the PROMPT (just the PROMPT proper) in cmd.exe and PowerShell? & http://gynvael.coldwind.pl/?id=130 +// +// It is "ANSI hack developped for the CMD.exe shell". +// +// +// However, it seems possible to do if the user is using Powershell instead of cmd.exe: +// http://stackoverflow.com/a/20666813/193165 +// +// TODO(dkg): cmd.exe is not really suited for this tool anyway, but Powershell is, so we +// should see if we can add colors for powershell. +// TODO(dkg): support colors for bash, zsh, etc. in Cygwin string makeColorFunction(string name, int code) { import std.conv : to; @@ -31,6 +49,8 @@ string makeColorFunction(string name, int code) return bashEscape(ret); case Escapes.zsh: return zshEscape(ret); + case Escapes.cmd: + return ""; } } `; diff --git a/git.d b/git.d index 20bc967..4f031f7 100644 --- a/git.d +++ b/git.d @@ -3,16 +3,32 @@ import std.conv : to; import std.datetime : Duration; import std.exception : enforce; import std.file : exists, isFile, dirEntries, DirEntry, readText, SpanMode; -import std.path : baseName, buildPath, relativePath; +import std.path : baseName, relativePath; import std.process; // : A whole lotta stuff import std.range : empty, front, back; import std.stdio : File; -import std.string : startsWith, strip, countchars, chompPrefix; +import std.string : startsWith, strip, countchars, chompPrefix, toStringz; import std.array : split; +import std.stdio; +import std.utf : toUTF16z; import time; import vcs; +version (Windows) version = PathFixNeeded; +version (Cygwin) version = PathFixNeeded; + +version (PathFixNeeded) +{ + import systempath : customBuildPath; +} +else +{ + import std.path : customBuildPath = buildPath; +} + + + // Fetches information about the Git repository, // or returns null if we are not in one. RepoStatus* getRepoStatus(Duration allottedTime) @@ -23,7 +39,6 @@ RepoStatus* getRepoStatus(Duration allottedTime) auto rootFinder = execute(["git", "rev-parse", "--show-toplevel"]); immutable repoRoot = rootFinder.output.strip(); - if (rootFinder.status != 0 || repoRoot.empty) return null; @@ -44,28 +59,13 @@ private: public // So std.parallelism can get at it StatusFlags asyncGetFlags(Duration allottedTime) { - // Currently we can only do this for Unix. - // Windows async pipe I/O (they call it "overlapped" I/O) - // is more... involved. - // TODO: Either write a Windows implementation or suck it up - // and do things synchronously in Windows. - import core.sys.posix.poll; - StatusFlags ret; - // Light off git status while we find the HEAD - auto pipes = pipeProcess(["git", "status", "--porcelain"], Redirect.stdout); - // If an exception gets thrown, be sure to cleanup the process. - scope(failure) { - kill(pipes.pid); - wait(pipes.pid); - } - // Local function for processing the output of git status. // See the docs for git status porcelain output void processPorcelainLine(string line) { - if (line is null) + if (line is null || line.length == 0) // 0 length line may happen on Windows return; // git status --porcelain spits out a two-character code @@ -92,6 +92,126 @@ StatusFlags asyncGetFlags(Duration allottedTime) } } + +version(Windows) +{ + import core.sys.windows.windows; + // NOTE(dkg): The wonders of the Win32 API are ... sigh. It's so ugly. + // I cobbled this together from some MSDN examples and stackoverflow + // and a lot of trial and error. So feel welcome to improve this. + STARTUPINFO startupInfo; + PROCESS_INFORMATION processInfo; + + HANDLE g_hChildStd_IN_Rd = NULL; + HANDLE g_hChildStd_IN_Wr = NULL; + HANDLE g_hChildStd_OUT_Rd = NULL; + HANDLE g_hChildStd_OUT_Wr = NULL; + + // Set the bInheritHandle flag so pipe handles are inherited. + SECURITY_ATTRIBUTES sa; + sa.nLength = SECURITY_ATTRIBUTES.sizeof; + sa.bInheritHandle = TRUE; + sa.lpSecurityDescriptor = NULL; + + auto cmdptr = toUTF16z("git status --porcelain"); + const int BUFSIZE = 4096; + uint timeLimit = cast(uint)allottedTime.total!"msecs"; + + // Create a pipe for the child process's STDOUT. + if (!CreatePipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &sa, 0)) { + throw new Exception("CreatePipe win32 api call failed."); + } + // Ensure the read handle to the pipe for STDOUT is not inherited + if (!SetHandleInformation(g_hChildStd_OUT_Rd, HANDLE_FLAG_INHERIT, 0)) { + throw new Exception("SetHandleInformation win32 api call failed."); + } + // Create a pipe for the child process's STDIN. + if (!CreatePipe(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &sa, 0)) { + throw new Exception("2nd CreatePipe win32 api call failed."); + } + // Ensure the write handle to the pipe for STDIN is not inherited. + if (!SetHandleInformation(g_hChildStd_IN_Wr, HANDLE_FLAG_INHERIT, 0)) { + throw new Exception("2nd SetHandleInformation win32 api call failed."); + } + + startupInfo.hStdError = g_hChildStd_OUT_Wr; + startupInfo.hStdOutput = g_hChildStd_OUT_Wr; + startupInfo.hStdInput = g_hChildStd_IN_Rd; + startupInfo.dwFlags |= STARTF_USESTDHANDLES; + startupInfo.cb = startupInfo.sizeof; + + if (CreateProcess(NULL, cast(wchar*)cmdptr, NULL, NULL, TRUE, 0, NULL, NULL, &startupInfo, &processInfo)) { + int waitResult = WaitForSingleObject(processInfo.hProcess, timeLimit); + + // Read output from the child process's pipe for STDOUT + // and write to the parent process's pipe for STDOUT. + // Stop when there is no more data. + string ReadFromPipe(PROCESS_INFORMATION piProcInfo) { + DWORD dwRead; + char[BUFSIZE] chBuf; + int bSuccess = false; + string outstring = ""; + + for (;;) { + bSuccess = ReadFile(g_hChildStd_OUT_Rd, cast(void*)chBuf.ptr, BUFSIZE, &dwRead, NULL); + + if (!bSuccess || dwRead == 0) break; + + string s = (cast(immutable(char)*)chBuf)[0..dwRead]; + outstring ~= s; + + if (dwRead < BUFSIZE) break; + } + dwRead = 0; + //for (;;) { + // bSuccess=ReadFile( g_hChildStd_ERR_Rd, chBuf, BUFSIZE, &dwRead, NULL); + // if( ! bSuccess || dwRead == 0 ) break; + + // string s(chBuf, dwRead); + // err += s; + + //} + return outstring; + } + + if (waitResult == WAIT_TIMEOUT) { + // terminate process + if (!TerminateProcess(processInfo.hProcess, 1)) { + // TODO(dkg): should we abandone ship here? + writeln("warning: git status call process did not return in time and termination failed"); + } + } else { + string gitStatusResult = ReadFromPipe(processInfo); + auto lines = gitStatusResult.split("\n"); + foreach (line; lines) { + processPorcelainLine(line); + } + } + + CloseHandle(processInfo.hProcess); + CloseHandle(processInfo.hThread); + } + + return ret; + +} else version(Posix) { + + // Currently we can only do this for Unix. + // Windows async pipe I/O (they call it "overlapped" I/O) + // is more... involved. + // TODO: Either write a Windows implementation or suck it up + // and do things synchronously in Windows. + import core.sys.posix.poll; + + // Light off git status while we find the HEAD + auto pipes = pipeProcess(["git", "status", "--porcelain"], Redirect.stdout); + // If an exception gets thrown, be sure to cleanup the process. + scope(failure) { + kill(pipes.pid); + wait(pipes.pid); + } + + // We need the actual file descriptor of the pipe so we can call poll immutable int fdes = core.stdc.stdio.fileno(pipes.stdout.getFP()); enforce(fdes >= 0, "fileno failed."); @@ -130,6 +250,12 @@ StatusFlags asyncGetFlags(Duration allottedTime) wait(pipes.pid); return ret; + +} else { + writeln("PLATFORM NOT SUPPORTED!"); + assert(0); // trips when version is not defined +} + } /// Gets the name of the current Git head, or a shortened SHA @@ -143,7 +269,7 @@ string getHead(string repoRoot, Duration allottedTime) // NOTE(dkg): added check to allow for git submodules // check if the .git file/folder is actually a folder // if it is a file, we are in a submodule - immutable gitFileOrFolder = buildPath(repoRoot, ".git"); + immutable gitFileOrFolder = customBuildPath(repoRoot, ".git"); if (exists(gitFileOrFolder) && isFile(gitFileOrFolder)) { string content = gitFileOrFolder.readAndStrip(); //Example content: gitdir: ../.git/modules/modulename @@ -155,8 +281,10 @@ string getHead(string repoRoot, Duration allottedTime) return ""; } } + immutable gitFolder = gitFileOrFolder; - immutable headPath = buildPath(repoRoot, ".git", "HEAD"); + //immutable headPath = customBuildPath(repoRoot, ".git", "HEAD"); + immutable headPath = customBuildPath(gitFolder, "HEAD"); immutable headSHA = headPath.readAndStrip(); // If we're on a branch head, .git/HEAD will look like @@ -169,12 +297,12 @@ string getHead(string repoRoot, Duration allottedTime) } // Otherwise let's go rummaging through the refs to find something - immutable refsPath = buildPath(repoRoot, ".git", "refs"); + immutable refsPath = customBuildPath(gitFolder, "refs"); string ret; // Let's check tags next - immutable tagsPath = buildPath(refsPath, "tags"); + immutable tagsPath = customBuildPath(refsPath, "tags"); ret = searchTagsForHead(tagsPath, headSHA); if (!ret.empty) return relativePath(ret, tagsPath); @@ -183,7 +311,7 @@ string getHead(string repoRoot, Duration allottedTime) // No need to check heads as we handled that case above. // Let's check remotes - immutable remotesPath = buildPath(refsPath, "remotes"); + immutable remotesPath = customBuildPath(refsPath, "remotes"); ret = searchDirectoryForHead(remotesPath, headSHA); if (!ret.empty) return relativePath(ret, remotesPath); @@ -192,7 +320,7 @@ string getHead(string repoRoot, Duration allottedTime) // We didn't find anything in remotes. Let's check packed-refs - immutable packedRefsPath = buildPath(repoRoot, ".git", "packed-refs"); + immutable packedRefsPath = customBuildPath(gitFolder, "packed-refs"); if (exists(packedRefsPath)) { auto packedRefs = File(packedRefsPath) .byLine @@ -241,10 +369,8 @@ string searchDirectoryForHead(string dir, string head) { return de.name.readAndStrip() == head; } - auto matchingRemotes = dirEntries(dir, SpanMode.depth, false) .filter!(f => isRefFile(f) && matchesHead(f)); - if (!matchingRemotes.empty) return matchingRemotes.front.name; else @@ -268,7 +394,6 @@ string searchTagsForHead(string dir, string head) auto matchingRemotes = dirEntries(dir, SpanMode.depth, false) .filter!(f => isRefFile(f) && matchesHead(f)); - if (!matchingRemotes.empty) return matchingRemotes.front.name; else diff --git a/help.d b/help.d index 8dfb885..e68ffaa 100644 --- a/help.d +++ b/help.d @@ -1,5 +1,5 @@ import std.stdio; -import std.c.stdlib : exit; +import core.stdc.stdlib : exit; /// Writes whatever you tell it and then exits the program successfully void writeAndSucceed(S...)(S toWrite) diff --git a/promptoglyph-path.d b/promptoglyph-path.d index 28c4df8..ed8586c 100644 --- a/promptoglyph-path.d +++ b/promptoglyph-path.d @@ -9,14 +9,22 @@ import std.array : array; import std.datetime : msecs; import std.file : getcwd; import std.getopt; -import std.path : pathSplitter, buildPath; +import std.path : pathSplitter; import std.process : environment; import std.range : empty, take; import std.traits : isSomeString; import std.utf : count, stride; +import std.stdio; import help; +// NOTE(dkg): see systempath.d and git.d for details about this +version (Windows) version = PathFixNeeded; +version (Cygwin) version = PathFixNeeded; + +import std.path : customBuildPath = buildPath; + + void main(string[] args) { import std.exception : ifThrown; @@ -38,15 +46,51 @@ void main(string[] args) writeAndFail(ex.msg, "\n", helpString); } - - immutable string home = environment["HOME"].ifThrown(""); - immutable string cwd = getcwd().ifThrown(environment["PWD"]).ifThrown("???"); + // NOTE(dkg): on Windows (under Cygwin even) env["HOME"] will include + // the drive letter and the path is a Windows style path + // Another problem is that home and cwd will result in + // home, cwd: C:\Cygwin\home\dkg, C:\Users\dkg\Projekte\d\promptd + // in Cygwin, so the homeToTilde function will not work here. + // When you convert both paths via cygpath to Unix style path they are + // different as well: + // home, cwd: /home/dkg, /cygdrive/c/Users/dkg/Projekte/d/promptd + version(PathFixNeeded) + { + import std.process : execute; + import std.string : strip, indexOf; + import std.array : replace; + + bool isCygWinEnv = environment.get("SHELL", "") != "" && + environment.get("TERM", "") != ""; + string home = environment["HOME"].ifThrown(""); + string cwd = getcwd().ifThrown(environment["PWD"]).ifThrown("???"); + + if (isCygWinEnv && home.indexOf(":") > -1) { + // sigh, yeah, see NOTE above as to why + // I want to get the real Unix style path here from Cygwin, + // not the Windows style one. + auto homePath = execute(["cygpath", "-u", home]); + home = homePath.output.strip(); + auto cwdPath = execute(["cygpath", "-u", cwd]); + cwd = cwdPath.output.strip().replace("\\", "/"); + } + } + else + { + immutable string home = environment["HOME"].ifThrown(""); + immutable string cwd = getcwd().ifThrown(environment["PWD"]).ifThrown("???"); + } string path = homeToTilde(cwd, home); - if (path.count >= shortenAt) path = shorten(path, shortenNumChars); + version (PathFixNeeded) + { + if (isCygWinEnv && path.indexOf("\\") > -1) + path = path.replace("\\", "/"); + } + write(path); } @@ -93,7 +137,7 @@ pure string homeToTilde(string cwd, string home) pure string shorten(string path, int numChars = 1) { auto pathTokens = pathSplitter(path).array; - + if (pathTokens.length < 2) return path; @@ -107,7 +151,7 @@ pure string shorten(string path, int numChars = 1) else rest = rest.map!(s => firstOf(s, numChars)).array; - return buildPath(rest ~ last); + return customBuildPath(rest ~ last); } unittest diff --git a/promptoglyph-vcs.d b/promptoglyph-vcs.d index 6c2d2be..2daf4ad 100644 --- a/promptoglyph-vcs.d +++ b/promptoglyph-vcs.d @@ -1,194 +1,227 @@ -module promptoglyph.vcs; - -import std.getopt; -import std.datetime : msecs; -import std.stdio : write; - -import color; -import git; -import help; -import vcs; - -struct StatusStringOptions { - string prefix = "["; - string suffix = "]"; - string indexedText = "✔"; - string modifiedText = "±"; - string untrackedText = "?"; -} - -void main(string[] args) -{ - uint timeLimit = 500; - bool noColor; - bool bash, zsh; - StatusStringOptions stringOptions; - - try { - getopt(args, - config.caseSensitive, - config.bundling, - "help|h", { writeAndSucceed(helpString); }, - "version|v", { writeAndSucceed(versionString); }, - "time-limit|t", &timeLimit, - "prefix|p", &stringOptions.prefix, - "indexed-text|i", &stringOptions.indexedText, - "modified-text|m", &stringOptions.modifiedText, - "untracked-text|u", &stringOptions.untrackedText, - "suffix|s", &stringOptions.suffix, - "no-color", &noColor, - "bash|b", &bash, - "zsh|z", &zsh); - } - catch (GetOptException ex) { - writeAndFail(ex.msg, "\n", helpString); - } - - if (bash && zsh) - writeAndFail("Both --bash and --zsh specified. Wat."); - - Escapes escapesToUse; - if (bash) - escapesToUse = Escapes.bash; - else if (zsh) - escapesToUse = Escapes.zsh; - else // Redundant (none is the default), but more explicit. - escapesToUse = Escapes.none; - - const Duration allottedTime = timeLimit.msecs; - - const RepoStatus* status = getRepoStatus(allottedTime); - - string statusString = stringRepOfStatus( - status, stringOptions, - noColor ? UseColor.no : UseColor.yes, - escapesToUse, - allottedTime); - - write(statusString); -} - -/** - * Gets a string representation of the status of the Git repo - * - * Params: - * allottedTime = The amount of time given to gather Git info. - * Git status will be killed if it does not complete in this much time. - * Since this is for a shell prompt, responsiveness is important. - * colors = Whether or not colored output is desired - * escapes = Whether or not ZSH escapes are needed. Ignored if no colors are desired. - * - */ -string stringRepOfStatus(const RepoStatus* status, const ref StatusStringOptions stringOptions, - UseColor colors, Escapes escapes, Duration allottedTime) -{ - import time; - - if (status is null) - return ""; - - // Local function that colors a source string if the colors flag is set. - string colorText(string source, - string function(Escapes) colorFunction) - { - if (!colors) - return source; - else - return colorFunction(escapes) ~ source; - } - - string head; - - if (!status.head.empty) - head = colorText(status.head, &cyan); - - string flags = " "; - - if (status.flags.indexed) - flags ~= colorText(stringOptions.indexedText, &green); - if (status.flags.modified) - flags ~= colorText(stringOptions.modifiedText, &yellow); // Yellow plus/minus - if (status.flags.untracked) - flags ~= colorText(stringOptions.untrackedText, &red); // Red quesiton mark - - // We don't want an extra space if there's nothing to show. - if (flags == " ") - flags = ""; - - string ret = head ~ flags ~ - colorText(stringOptions.suffix, &resetColor); - - if (pastTime(allottedTime)) - ret = "T " ~ ret; - - return stringOptions.prefix ~ ret; -} - -string versionString = q"EOS -promptoglyph-vcs by Matt Kline, version 0.5 -Part of the promptoglyph tool set -EOS"; - -string helpString = q"EOS -usage: promptoglyph-vcs [-t ] - -Options: - - --help, -h - Display this help text - - --version, -v - Display the version info - - --time-limit, -t - The maximum amount of time the program can run before exiting, - in milliseconds. Defaults to 500 milliseconds. - Running "git status" can take a long time for big or complex - repositories, but since this program is for a prompt, - we can't delay an arbitrary amount of time without annoying the user. - If it takes longer than this amount of time to get the repo status, - we prematurely kill "git status" and display whatever information - was received so far. The hope is that in subsequent runs, "git status" will - complete in time since your operating system caches recently-accessed - files and directories. - - --no-color - Disables colored output, which is on by default - - --prefix, -p - Text to prepend to the VCS information (if in a VCS directory) - - --untracked-text, -u - Text to display when the VCS indicates untracked files - (if in a VCS directory) - - --modified-text, -m - Text to display when the VCS indicates files modified since the last commit - (if in a VCS directory) - - --indexed-text, -i - Text to display when the VSC indicates files ready to commit - (if in a VCS directory) - - --suffix, -s - Text to append to the VCS information (if in a VCS directory) - - --bash, -b - Used to emit additional escapes needed for color sequences in Bash prompts. - Ignored if --no-color is specified. - - --zsh, -z - Used to emit additional escapes needed for color sequences in ZSH prompts. - Ignored if --no-color is specified. - -promptoglyph-vcs is designed to be part of a shell prompt. -It prints a quick, symbolic look at the status of a Git repository -if you are currently in one and nothing otherwise. Output looks like - [master ✔±?] -where "master" is the current branch, ? indicates untracked files, -± indicates changed but unstaged files, and ✔ indicates files staged -in the index. If "git status" could not run in a timely manner to get this info -(see --time-limit above), a T is placed in front. -Future plans include additional info (like when merging), -and possibly Subversion and Mercurial support. -EOS"; +module promptoglyph.vcs; + +import std.getopt; +import std.datetime : msecs; +import std.stdio : write; +import std.array : replace; + +import color; +import git; +import help; +import vcs; + +// TODO(dkg): somehow fix the unicode support on Windows' cmd.exe +// cmd.exe does not support fancy unicode chars in the output for whatever reason +// see http://stackoverflow.com/questions/14109024/how-to-make-unicode-charset-in-cmd-exe-by-default +// Another thing is that cmd.exe is not really suited for this tool anyway, but Powershell is, so we +// should see if we can add fancy unicode chars for powershell users. +// Maybe use program arguments again, like we already do for coloring or +// bash/zsh specific escape codes. + +version(Windows) +{ + const string defaultIndexedText = "i"; + const string defaultModifiedText = "m"; + const string defaultUntrackedText = "u"; +} +else +{ + const string defaultIndexedText = "✔"; + const string defaultModifiedText = "±"; + const string defaultUntrackedText = "?"; +} + +struct StatusStringOptions { + string prefix = "["; + string suffix = "]"; + string indexedText = defaultIndexedText; + string modifiedText = defaultModifiedText; + string untrackedText = defaultUntrackedText; +} + +void main(string[] args) +{ + uint timeLimit = 500; + bool noColor; + bool bash, zsh; + StatusStringOptions stringOptions; + + try { + getopt(args, + config.caseSensitive, + config.bundling, + "help|h", { writeAndSucceed(helpString); }, + "version|v", { writeAndSucceed(versionString); }, + "time-limit|t", &timeLimit, + "prefix|p", &stringOptions.prefix, + "indexed-text|i", &stringOptions.indexedText, + "modified-text|m", &stringOptions.modifiedText, + "untracked-text|u", &stringOptions.untrackedText, + "suffix|s", &stringOptions.suffix, + "no-color", &noColor, + "bash|b", &bash, + "zsh|z", &zsh); + } + catch (GetOptException ex) { + writeAndFail(ex.msg, "\n", helpString); + } + + if (bash && zsh) + writeAndFail("Both --bash and --zsh specified. Wat."); + + Escapes escapesToUse; + version(Windows) { + escapesToUse = Escapes.cmd; + } else version(Posix) { + if (bash) + escapesToUse = Escapes.bash; + else if (zsh) + escapesToUse = Escapes.zsh; + else // Redundant (none is the default), but more explicit. + escapesToUse = Escapes.none; + } + + const Duration allottedTime = timeLimit.msecs; + + const RepoStatus* status = getRepoStatus(allottedTime); + + string statusString = stringRepOfStatus( + status, stringOptions, + noColor ? UseColor.no : UseColor.yes, + escapesToUse, + allottedTime); + + write(statusString); +} + +/** + * Gets a string representation of the status of the Git repo + * + * Params: + * allottedTime = The amount of time given to gather Git info. + * Git status will be killed if it does not complete in this much time. + * Since this is for a shell prompt, responsiveness is important. + * colors = Whether or not colored output is desired + * escapes = Whether or not ZSH escapes are needed. Ignored if no colors are desired. + * + */ +string stringRepOfStatus(const RepoStatus* status, const ref StatusStringOptions stringOptions, + UseColor colors, Escapes escapes, Duration allottedTime) +{ + import time; + + if (status is null) + return ""; + + // Local function that colors a source string if the colors flag is set. + string colorText(string source, + string function(Escapes) colorFunction) + { + if (!colors) + return source; + else + return colorFunction(escapes) ~ source; + } + + string head; + + if (!status.head.empty) + head = colorText(status.head, &cyan); + + string flags = " "; + + if (status.flags.indexed) + flags ~= colorText(stringOptions.indexedText, &green); + if (status.flags.modified) + flags ~= colorText(stringOptions.modifiedText, &yellow); // Yellow plus/minus + if (status.flags.untracked) + flags ~= colorText(stringOptions.untrackedText, &red); // Red quesiton mark + + // We don't want an extra space if there's nothing to show. + if (flags == " ") + flags = ""; + + string ret = head ~ flags ~ + colorText(stringOptions.suffix, &resetColor); + + if (pastTime(allottedTime)) + ret = "T " ~ ret; + + return stringOptions.prefix ~ ret; +} + +const string versionString = q"EOS +promptoglyph-vcs by Matt Kline, version 0.5 +Part of the promptoglyph tool set +EOS"; + +const string helpStringTemp = q"EOS +usage: promptoglyph-vcs [-t ] + +Options: + + --help, -h + Display this help text + + --version, -v + Display the version info + + --time-limit, -t + The maximum amount of time the program can run before exiting, + in milliseconds. Defaults to 500 milliseconds. + Running "git status" can take a long time for big or complex + repositories, but since this program is for a prompt, + we can't delay an arbitrary amount of time without annoying the user. + If it takes longer than this amount of time to get the repo status, + we prematurely kill "git status" and display whatever information + was received so far. The hope is that in subsequent runs, "git status" will + complete in time since your operating system caches recently-accessed + files and directories. + + --no-color + Disables colored output, which is on by default + + --prefix, -p + Text to prepend to the VCS information (if in a VCS directory) + + --untracked-text, -u + Text to display when the VCS indicates untracked files + (if in a VCS directory) + + --modified-text, -m + Text to display when the VCS indicates files modified since the last commit + (if in a VCS directory) + + --indexed-text, -i + Text to display when the VSC indicates files ready to commit + (if in a VCS directory) + + --suffix, -s + Text to append to the VCS information (if in a VCS directory) + + --bash, -b + Used to emit additional escapes needed for color sequences in Bash prompts. + Ignored if --no-color is specified. + Ignored on Windows. + + --zsh, -z + Used to emit additional escapes needed for color sequences in ZSH prompts. + Ignored if --no-color is specified. + Ignored on Windows. + +promptoglyph-vcs is designed to be part of a shell prompt. +It prints a quick, symbolic look at the status of a Git repository +if you are currently in one and nothing otherwise. Output looks like + [master {indexedText}{modifiedText}{untrackedText}] +where "master" is the current branch, {untrackedText} indicates untracked files, +{modifiedText} indicates changed but unstaged files, and {indexedText} indicates files staged +in the index. If "git status" could not run in a timely manner to get this info +(see --time-limit above), a T is placed in front. +Future plans include additional info (like when merging), +and possibly Subversion and Mercurial support. +EOS"; + +const string helpString = helpStringTemp + .replace("{indexedText}", defaultIndexedText) + .replace("{modifiedText}", defaultModifiedText) + .replace("{untrackedText}", defaultUntrackedText); diff --git a/systempath.d b/systempath.d new file mode 100644 index 0000000..7d0bf60 --- /dev/null +++ b/systempath.d @@ -0,0 +1,126 @@ +import std.stdio; +import std.process; +import std.string : startsWith, strip, indexOf; +import std.array : replace; +import core.vararg; + +// On Windows the following can happen: +// +// The user has installed git normally via Chocolately or MSI package +// so it can be used in cmd.exe. The user also has git installed +// as part of Cygwin via the Cygwin package manager. +// +// Depending on which one is called the output of the following command +// to get the project's root folder +// +// auto rootFinder = execute(["git", "rev-parse", "--show-toplevel"]); +// +// is either C:\path\to\repro or /path/to/repro. +// +// If the cmd.exe version is called within a Cygwin terminal or vice-versa +// then all kinds of things go wrong with file and directory access. +// +// The problem is that D's standard library uses the +// version(Windows) { ... Win32 API calls ... } +// version(Posix) { ... Posix calls ... } +// compiler directives to provide file access and read/write files. +// So internally it uses the Win32 API on Windows which expects C:\path style +// paths, unlike what Cygwin's git version gives us, which is /path style paths. +// +// So the file reading comes down crashing. +// +// If I compile in Cyginw then everything works as expected? +// No, unfortunately not, if the D compiler is installed using the MSI package +// (that is, it is not a Cygwin package). Not sure what would happen if you +// installed D via a Cygwin package or even compile it from source in Cygwin though. +// Feel free to investigate that route. +// +// The solution for this case (Cygwin git, compiled promptoglyph-vcs.exe with +// dmd.exe installed via non-cygwin-package, executing within Cygwin) is to +// convert the Unix style path to a Windows style one with the handy cygpath +// tool. +// +version (Windows) version = PathFixNeeded; +version (Cygwin) version = PathFixNeeded; + +private: + +bool isCygWinEnv = false; +bool testedForCygwin = false; + +version (PathFixNeeded) +{ + import std.path : buildNormalizedPath; + // Runtime Cygwin detection + // NOTE(dkg): If you have a cleaner/better solution, please let me know. Thanks. + // + // UID is empty even in cygwin??? + // HOME is C:\Cygwin\home\ and not (as expected) /home/ + // + //void environmentTest() + //{ + // writeln("UID is ", environment.get("UID", "empty")); + // writeln("HOME is ", environment.get("HOME", "empty")); + // writeln("SHELL is ", environment.get("SHELL", "empty")); + // writeln("TERM is ", environment.get("TERM", "empty")); + //} + + // NOTE(dkg): While this works, it is not particularly elegant. + // Maybe this could be improved by using program arguments + // instead to force a particular path style? So in + // Cygwin's bash you would pass something like "--cygwin" + // and then would just convert the paths always, so the + // dynamic check during runtime would not be needed. + public string customBuildPath(...) + { + if (!testedForCygwin) { + // NOTE(dkg): If you know a better way to check during runtime + // whether or not we are in a Cygwin shell, then please + // let me know. + isCygWinEnv = environment.get("SHELL", "") != "" && + environment.get("TERM", "") != ""; + testedForCygwin = true; + } + + string s = ""; + for (int i = 0; i < _arguments.length; i++) + { + if (_arguments[i] == typeid(string[])) { + string[] elements = va_arg!(string[])(_argptr); + foreach (element; elements) + { + s = buildNormalizedPath(s, element); + } + } else { + string element = va_arg!(string)(_argptr); + s = buildNormalizedPath(s, element); + } + } + if (isCygWinEnv) { + // on cygwin - replace \ with / + // also make sure that we convert the path to a Windows path + // that means convernt /path to C:\path + if (s.indexOf(":") <= -1) { + s = s.replace("\\", "/"); + if (s.startsWith("/")) { + auto pathConversion = execute(["cygpath", "-w", s]); + if (pathConversion.status != 0) { + writeln("path could not be converted to Windows compatible path: ", s); + assert(0); // force crash + } + s = pathConversion.output.strip(); + } else { + //writeln("path is not an absolute path: ", s); + //assert(0); // force crash + } + } + } + return s; + } // customBuildPath + +} +else +{ + //import std.path : customBuildPath = buildPath; +} +