From 330aa863b2464accf60806940f6eff91d70709cc Mon Sep 17 00:00:00 2001 From: Daniel Kurashige-Gollub Date: Mon, 7 Mar 2016 17:52:57 +0900 Subject: [PATCH 1/2] fixes issue #14 compiling on Win 7 now works This commit adds custom build.bat for Windows and changes the code so that it can be compiled on Windows. The resulting exe files run without issues in Powershell, cmd.exe and Cygwin. The code is not the cleanest and could be improved and there are also a few Notes and Todos throughout it that could be addressed in the future. For now the big breakthrough is that "it compiles" on Windows. --- .gitignore | 118 ++++++++++++- Makefile | 4 +- README.md | 48 ++++- build.bat | 41 +++++ color.d | 22 ++- git.d | 182 ++++++++++++++++--- help.d | 2 +- promptoglyph-path.d | 58 +++++- promptoglyph-vcs.d | 421 ++++++++++++++++++++++++-------------------- systempath.d | 126 +++++++++++++ 10 files changed, 782 insertions(+), 240 deletions(-) create mode 100644 build.bat create mode 100644 systempath.d 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..fd477b1 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,127 @@ 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) { + writeln("On 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 +251,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 +270,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 +282,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 +298,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 +312,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 +321,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 +370,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 +395,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; +} + From 708258d695e5c4671fa5f2ca019cc0fb461034a3 Mon Sep 17 00:00:00 2001 From: Daniel Kurashige-Gollub Date: Tue, 8 Mar 2016 09:37:41 +0900 Subject: [PATCH 2/2] removed superfluous debug writeln --- git.d | 1 - 1 file changed, 1 deletion(-) diff --git a/git.d b/git.d index fd477b1..4f031f7 100644 --- a/git.d +++ b/git.d @@ -195,7 +195,6 @@ version(Windows) return ret; } else version(Posix) { - writeln("On Posix"); // Currently we can only do this for Unix. // Windows async pipe I/O (they call it "overlapped" I/O)