├── .gitignore ├── .paket ├── Paket.Restore.targets ├── paket.bootstrapper.exe ├── paket.exe └── paket.targets ├── FsMenu.sln ├── README.md ├── RELEASE_NOTES.md ├── appveyor.yaml ├── build.cmd ├── build.fsx ├── build.sh ├── misc ├── README.md ├── sample.gif ├── sample2.gif ├── sample3.gif ├── sample4.gif └── sample5.gif ├── paket.dependencies ├── paket.lock ├── src └── FsMenu.Core │ ├── AssemblyInfo.fs │ ├── Colors.fs │ ├── FsMenu.Core.fsproj │ ├── FsMenu.fs │ └── paket.references └── tests └── FsMenu.Console ├── App.config ├── FsMenu.Console.fsproj ├── Program.fs └── paket.references /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # .NET Core 46 | project.lock.json 47 | project.fragment.lock.json 48 | artifacts/ 49 | **/Properties/launchSettings.json 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # Visual Studio code coverage results 117 | *.coverage 118 | *.coveragexml 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | *.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignorable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | *.ndf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | node_modules/ 238 | 239 | # Typescript v1 declaration files 240 | typings/ 241 | 242 | # Visual Studio 6 build log 243 | *.plg 244 | 245 | # Visual Studio 6 workspace options file 246 | *.opt 247 | 248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 249 | *.vbw 250 | 251 | # Visual Studio LightSwitch build output 252 | **/*.HTMLClient/GeneratedArtifacts 253 | **/*.DesktopClient/GeneratedArtifacts 254 | **/*.DesktopClient/ModelManifest.xml 255 | **/*.Server/GeneratedArtifacts 256 | **/*.Server/ModelManifest.xml 257 | _Pvt_Extensions 258 | 259 | # Paket dependency manager 260 | .paket/paket.exe 261 | paket-files/ 262 | 263 | # FAKE - F# Make 264 | .fake/ 265 | 266 | # JetBrains Rider 267 | .idea/ 268 | *.sln.iml 269 | 270 | # CodeRush 271 | .cr/ 272 | 273 | # Python Tools for Visual Studio (PTVS) 274 | __pycache__/ 275 | *.pyc 276 | 277 | # Cake - Uncomment if you are using it 278 | # tools/** 279 | # !tools/packages.config 280 | 281 | # Telerik's JustMock configuration file 282 | *.jmconfig 283 | 284 | # BizTalk build output 285 | *.btp.cs 286 | *.btm.cs 287 | *.odx.cs 288 | *.xsd.cs 289 | -------------------------------------------------------------------------------- /.paket/Paket.Restore.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 8 | 9 | true 10 | $(MSBuildThisFileDirectory) 11 | $(MSBuildThisFileDirectory)..\ 12 | $(PaketRootPath)paket-files\paket.restore.cached 13 | $(PaketRootPath)paket.lock 14 | /Library/Frameworks/Mono.framework/Commands/mono 15 | mono 16 | 17 | $(PaketRootPath)paket.exe 18 | $(PaketToolsPath)paket.exe 19 | "$(PaketExePath)" 20 | $(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" 21 | 22 | 23 | <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)")) 24 | dotnet "$(PaketExePath)" 25 | 26 | 27 | "$(PaketExePath)" 28 | 29 | $(PaketRootPath)paket.bootstrapper.exe 30 | $(PaketToolsPath)paket.bootstrapper.exe 31 | "$(PaketBootStrapperExePath)" 32 | $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)" 33 | 34 | 35 | 36 | 37 | true 38 | true 39 | 40 | 41 | 42 | 43 | 44 | 45 | true 46 | $(NoWarn);NU1603 47 | 48 | 49 | 50 | 51 | /usr/bin/shasum $(PaketRestoreCacheFile) | /usr/bin/awk '{ print $1 }' 52 | /usr/bin/shasum $(PaketLockFilePath) | /usr/bin/awk '{ print $1 }' 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) 66 | $([System.IO.File]::ReadAllText('$(PaketLockFilePath)')) 67 | true 68 | false 69 | true 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | $(MSBuildProjectDirectory)\obj\$(MSBuildProjectFile).paket.references.cached 79 | 80 | $(MSBuildProjectFullPath).paket.references 81 | 82 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references 83 | 84 | $(MSBuildProjectDirectory)\paket.references 85 | $(MSBuildProjectDirectory)\obj\$(MSBuildProjectFile).$(TargetFramework).paket.resolved 86 | true 87 | references-file-or-cache-not-found 88 | 89 | 90 | 91 | 92 | $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)')) 93 | $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)')) 94 | references-file 95 | false 96 | 97 | 98 | 99 | 100 | false 101 | 102 | 103 | 104 | 105 | true 106 | target-framework '$(TargetFramework)' 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) 124 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) 125 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4]) 126 | 127 | 128 | %(PaketReferencesFileLinesInfo.PackageVersion) 129 | All 130 | 131 | 132 | 133 | 134 | $(MSBuildProjectDirectory)/obj/$(MSBuildProjectFile).paket.clitools 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0]) 144 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1]) 145 | 146 | 147 | %(PaketCliToolFileLinesInfo.PackageVersion) 148 | 149 | 150 | 151 | 155 | 156 | 157 | 158 | 159 | 160 | false 161 | 162 | 163 | 164 | 165 | 166 | <_NuspecFilesNewLocation Include="$(BaseIntermediateOutputPath)$(Configuration)\*.nuspec"/> 167 | 168 | 169 | 170 | $(MSBuildProjectDirectory)/$(MSBuildProjectFile) 171 | true 172 | false 173 | true 174 | $(BaseIntermediateOutputPath)$(Configuration) 175 | $(BaseIntermediateOutputPath) 176 | 177 | 178 | 179 | <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.nuspec"/> 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 232 | 233 | 274 | 275 | 276 | 277 | -------------------------------------------------------------------------------- /.paket/paket.bootstrapper.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolaiw/FsMenu/4f52cf9747675d15e1e0994b5775227d9e708475/.paket/paket.bootstrapper.exe -------------------------------------------------------------------------------- /.paket/paket.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolaiw/FsMenu/4f52cf9747675d15e1e0994b5775227d9e708475/.paket/paket.exe -------------------------------------------------------------------------------- /.paket/paket.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | true 7 | $(MSBuildThisFileDirectory) 8 | $(MSBuildThisFileDirectory)..\ 9 | /Library/Frameworks/Mono.framework/Commands/mono 10 | mono 11 | 12 | 13 | 14 | 15 | $(PaketRootPath)paket.exe 16 | $(PaketToolsPath)paket.exe 17 | "$(PaketExePath)" 18 | $(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" 19 | 20 | 21 | 22 | 23 | 24 | $(MSBuildProjectFullPath).paket.references 25 | 26 | 27 | 28 | 29 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references 30 | 31 | 32 | 33 | 34 | $(MSBuildProjectDirectory)\paket.references 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | $(PaketCommand) restore --references-files "$(PaketReferences)" 47 | 48 | RestorePackages; $(BuildDependsOn); 49 | 50 | 51 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /FsMenu.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26430.15 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".paket", ".paket", "{D23FE473-F7D0-4C29-8BDD-18BCDE8491A6}" 7 | ProjectSection(SolutionItems) = preProject 8 | paket.dependencies = paket.dependencies 9 | paket.lock = paket.lock 10 | EndProjectSection 11 | EndProject 12 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsMenu.Core", "src\FsMenu.Core\FsMenu.Core.fsproj", "{B1E153E6-8A13-4D07-81F5-161FD571C1BE}" 13 | EndProject 14 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsMenu.Console", "tests\FsMenu.Console\FsMenu.Console.fsproj", "{EA938605-9ECD-45E1-BFEF-8F6B1F5AEFA4}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D5BCD177-432F-4089-B156-7FA1FD8EDA05}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{982A1C3A-0329-4D75-A083-1EADEF93A56F}" 19 | EndProject 20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{44DB43C9-DF99-4A51-89DA-3C42A1D57F5C}" 21 | ProjectSection(SolutionItems) = preProject 22 | appveyor.yaml = appveyor.yaml 23 | build.cmd = build.cmd 24 | build.fsx = build.fsx 25 | build.sh = build.sh 26 | README.md = README.md 27 | RELEASE_NOTES.md = RELEASE_NOTES.md 28 | EndProjectSection 29 | EndProject 30 | Global 31 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 32 | Debug|Any CPU = Debug|Any CPU 33 | Release|Any CPU = Release|Any CPU 34 | EndGlobalSection 35 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 36 | {B1E153E6-8A13-4D07-81F5-161FD571C1BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {B1E153E6-8A13-4D07-81F5-161FD571C1BE}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {B1E153E6-8A13-4D07-81F5-161FD571C1BE}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {B1E153E6-8A13-4D07-81F5-161FD571C1BE}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {EA938605-9ECD-45E1-BFEF-8F6B1F5AEFA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {EA938605-9ECD-45E1-BFEF-8F6B1F5AEFA4}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {EA938605-9ECD-45E1-BFEF-8F6B1F5AEFA4}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {EA938605-9ECD-45E1-BFEF-8F6B1F5AEFA4}.Release|Any CPU.Build.0 = Release|Any CPU 44 | EndGlobalSection 45 | GlobalSection(SolutionProperties) = preSolution 46 | HideSolutionNode = FALSE 47 | EndGlobalSection 48 | GlobalSection(NestedProjects) = preSolution 49 | {B1E153E6-8A13-4D07-81F5-161FD571C1BE} = {D5BCD177-432F-4089-B156-7FA1FD8EDA05} 50 | {EA938605-9ECD-45E1-BFEF-8F6B1F5AEFA4} = {982A1C3A-0329-4D75-A083-1EADEF93A56F} 51 | EndGlobalSection 52 | EndGlobal 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FsMenu 2 | 3 | > A very small DSL to create an interactive cli. 4 | 5 | ### Build status 6 | 7 | **Master Branch** 8 | 9 | [![Build status](https://ci.appveyor.com/api/projects/status/tngsbj4u54o90fit/branch/master?svg=true)](https://ci.appveyor.com/project/Jallah/fsmenu/branch/master) 10 | 11 | ### Nuget 12 | 13 | Inc. 14 | 15 | ### Usage 16 | 17 | #### The DSL: 18 | 19 | ```fsharp 20 | // Creates a new menu 21 | let Menu = Sub 22 | 23 | // Render sub menu 24 | let (+>) name entry : (string * MenuEntry) = (name,entry) 25 | 26 | // Execute action and exit 27 | let (=>) s f = (s, Action (fun () -> f(); Exit)) 28 | 29 | // Execute action and render the previous menu 30 | let (<+) s f = (s, Action (fun () -> f(); NavigateBack)) 31 | 32 | // Execute action and render the menu where you come frome 33 | let (<+=) s f = (s, Action (fun () -> f(); Stay)) 34 | ``` 35 | 36 | #### Example: 37 | 38 | ```fsharp 39 | 40 | let testFunc() = 41 | printfn "selected Sub Sub 3" 42 | printf "handle some input: " 43 | let input = Console.ReadLine() 44 | // Do some stuff with input 45 | () 46 | 47 | printfn "Use Keys: UP-Arrow DOWN-Arrow ENTER BACK-SPACE\n" 48 | 49 | let mutable yesOrNo = "" 50 | 51 | let test = 52 | Menu [ 53 | "Item 1" => (fun () -> printf "selected Item 1") 54 | "Item 2" +> 55 | Menu [ 56 | "Sub 1" +> 57 | Menu [ 58 | "Sub Sub 1" => (fun () -> printf "selected Sub Sub 1") 59 | "Sub Sub 2" +> 60 | Menu [ 61 | "yes" <+ (fun () -> yesOrNo <- "--yes") 62 | "no " <+ (fun () -> yesOrNo <- "--no") ] 63 | "Sub Sub 3" <+ testFunc] 64 | "Sub 2" => (fun () -> printf "selected Sub 2") 65 | "Sub 3" => (fun () -> printf "exec some command with param %s" yesOrNo)]] 66 | 67 | 68 | render test "<--" 69 | ``` 70 | 71 | > Instead of `(fun () -> printf ...` you could pass any `unit -> unit` function. 72 | 73 | 74 | #### Will turn into 75 | 76 | ![](https://github.com/nicolaiw/FsMenu/blob/master/misc/sample.gif) 77 | 78 | #### Want some color ? :) 79 | 80 | ```fsharp 81 | renderWithColoredEmphaziser test "<--" Color.Green 82 | ``` 83 | 84 | #### Here it is 85 | 86 | ![](https://github.com/nicolaiw/FsMenu/blob/master/misc/sample3.gif) 87 | 88 | ### It is also possible to emphazise the entry 89 | 90 | ```fsharp 91 | renderWithColoredEntry test Color.Cyan 92 | ``` 93 | 94 | ![](https://github.com/nicolaiw/FsMenu/blob/master/misc/sample4.gif) 95 | 96 | ### Or Both, the emphaziser and the entry 97 | 98 | ```fsharp 99 | renderWithColoredLine test "<--" Color.Green 100 | ``` 101 | 102 | ![](https://github.com/nicolaiw/FsMenu/blob/master/misc/sample5.gif) 103 | 104 | #### Another example 105 | 106 | [Here](https://github.com/nicolaiw/FsMenu/tree/master/misc) 107 | 108 | ### Build 109 | 110 | + On Windows run build.cmd 111 | + On Linux run build.sh 112 | 113 | ### TODO ( contributes are very welcome :D ) 114 | + Add support for multiselect list and single select list 115 | + Create NuGet Package using Paket 116 | + Clean build.fsx 117 | + Refactoring: The goal is too use the render funcion like `render test <| withColoredEmphaziser "<--" Color.Green` or something like that 118 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | #### 0.0.1 2 | * Use Paket for dependency management 3 | * Use Fake (F# Make) for builds 4 | * Add appveyor build status 5 | 6 | #### 0.0.2 7 | * Posibility to colorize the emphazizer 8 | * Posibility to colorize the entry 9 | * Rename namspaces and modules 10 | 11 | #### 0.3.0 12 | * Wrape renderColored with different functions 13 | * Rendering with a colored emphaziser can now be accomplished using renderWithColoredEmphaziser 14 | 15 | -------------------------------------------------------------------------------- /appveyor.yaml: -------------------------------------------------------------------------------- 1 | init: 2 | - git config --global core.autocrlf input 3 | build_script: 4 | - cmd: build.cmd 5 | test: off 6 | version: 0.0.1.{build} 7 | artifacts: 8 | - path: bin 9 | name: bin -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | cls 3 | 4 | .paket\paket.bootstrapper.exe 5 | if errorlevel 1 ( 6 | exit /b %errorlevel% 7 | ) 8 | 9 | .paket\paket.exe restore 10 | if errorlevel 1 ( 11 | exit /b %errorlevel% 12 | ) 13 | 14 | IF NOT EXIST build.fsx ( 15 | .paket\paket.exe update 16 | packages\FAKE\tools\FAKE.exe init.fsx 17 | ) 18 | packages\FAKE\tools\FAKE.exe build.fsx %* -------------------------------------------------------------------------------- /build.fsx: -------------------------------------------------------------------------------- 1 | 2 | // -------------------------------------------------------------------------------------- 3 | // FAKE build script 4 | // -------------------------------------------------------------------------------------- 5 | 6 | #r @"packages/FAKE/tools/FakeLib.dll" 7 | open Fake 8 | open Fake.Git 9 | open Fake.AssemblyInfoFile 10 | open Fake.ReleaseNotesHelper 11 | open System 12 | open System.IO 13 | //#if MONO 14 | //#else 15 | //#load "packages/SourceLink.Fake/tools/Fake.fsx" 16 | //open SourceLink 17 | //#endif 18 | 19 | 20 | 21 | // -------------------------------------------------------------------------------------- 22 | // START TODO: Provide project-specific details below 23 | // -------------------------------------------------------------------------------------- 24 | 25 | // Information about the project are used 26 | // - for version and project name in generated AssemblyInfo file 27 | // - by the generated NuGet package 28 | // - to run tests and to publish documentation on GitHub gh-pages 29 | // - for documentation, you also need to edit info in "docs/tools/generate.fsx" 30 | 31 | // The name of the project 32 | // (used by attributes in AssemblyInfo, name of a NuGet package and directory in 'src') 33 | let project = "FsMenu" 34 | 35 | // Short summary of the project 36 | // (used as description in AssemblyInfo and as a short summary for NuGet package) 37 | let summary = "Small DSL to create an interactive cli" 38 | 39 | // Longer description of the project 40 | // (used as a description for NuGet package; line breaks are automatically cleaned up) 41 | let description = "Small DSL to create an interactive cli" 42 | 43 | // List of author names (for NuGet package) 44 | let authors = [ "Wirtz N." ] 45 | 46 | // Tags for your project (for NuGet package) 47 | let tags = "FsMenu F# CLI cli" 48 | 49 | // File system information 50 | let solutionFile = "FsMenu.sln" 51 | 52 | // Pattern specifying assemblies to be tested using NUnit 53 | let testAssemblies = "tests/**/bin/Release/*Tests*.dll" 54 | 55 | // Git configuration (used for publishing documentation in gh-pages branch) 56 | // The profile where the project is posted 57 | let gitOwner = "nicolaiw" 58 | let gitHome = "https://github.com/" + gitOwner 59 | 60 | // The name of the project on GitHub 61 | let gitName = "FsMenu" 62 | 63 | // The url for the raw files hosted 64 | let gitRaw = environVarOrDefault "gitRaw" "https://raw.github.com/nicolaiw" 65 | 66 | // -------------------------------------------------------------------------------------- 67 | // END TODO: The rest of the file includes standard build steps 68 | // -------------------------------------------------------------------------------------- 69 | 70 | // Read additional information from the release notes document 71 | let release = LoadReleaseNotes "RELEASE_NOTES.md" 72 | 73 | // Helper active pattern for project types 74 | let (|Fsproj|Csproj|Vbproj|) (projFileName:string) = 75 | match projFileName with 76 | | f when f.EndsWith("fsproj") -> Fsproj 77 | | f when f.EndsWith("csproj") -> Csproj 78 | | f when f.EndsWith("vbproj") -> Vbproj 79 | | _ -> failwith (sprintf "Project file %s not supported. Unknown project type." projFileName) 80 | 81 | // Generate assembly info files with the right version & up-to-date information 82 | Target "AssemblyInfo" (fun _ -> 83 | let getAssemblyInfoAttributes projectName = 84 | [ Attribute.Title (projectName) 85 | Attribute.Product project 86 | Attribute.Description summary 87 | Attribute.Version release.AssemblyVersion 88 | Attribute.FileVersion release.AssemblyVersion] 89 | 90 | let getProjectDetails projectPath = 91 | let projectName = System.IO.Path.GetFileNameWithoutExtension(projectPath) 92 | ( projectPath, 93 | projectName, 94 | System.IO.Path.GetDirectoryName(projectPath), 95 | (getAssemblyInfoAttributes projectName) 96 | ) 97 | 98 | !! "src/**/*.??proj" 99 | |> Seq.map getProjectDetails 100 | |> Seq.iter (fun (projFileName, projectName, folderName, attributes) -> 101 | match projFileName with 102 | | Fsproj -> CreateFSharpAssemblyInfo (folderName @@ "AssemblyInfo.fs") attributes 103 | | Csproj -> CreateCSharpAssemblyInfo ((folderName @@ "Properties") @@ "AssemblyInfo.cs") attributes 104 | | Vbproj -> CreateVisualBasicAssemblyInfo ((folderName @@ "My Project") @@ "AssemblyInfo.vb") attributes 105 | ) 106 | ) 107 | 108 | // Copies binaries from default VS location to expected bin folder 109 | // But keeps a subdirectory structure for each project in the 110 | // src folder to support multiple project outputs 111 | Target "CopyBinaries" (fun _ -> 112 | !! "src/**/*.??proj" 113 | |> Seq.map (fun f -> ((System.IO.Path.GetDirectoryName f) @@ "bin/Release", "bin" @@ (System.IO.Path.GetFileNameWithoutExtension f))) 114 | |> Seq.iter (fun (fromDir, toDir) -> CopyDir toDir fromDir (fun _ -> true)) 115 | ) 116 | 117 | // -------------------------------------------------------------------------------------- 118 | // Clean build results 119 | 120 | Target "Clean" (fun _ -> 121 | CleanDirs ["bin"; "temp"] 122 | ) 123 | 124 | Target "CleanDocs" (fun _ -> 125 | CleanDirs ["docs/output"] 126 | ) 127 | 128 | // -------------------------------------------------------------------------------------- 129 | // Build library & test project 130 | 131 | #if MONO 132 | traceImportant "MONO" 133 | 134 | Target "Build" (fun _ -> 135 | !! solutionFile 136 | //|> MSBuildRelease "" "Rebuild" 137 | |> MSBuild "" "Rebuild" ([ 138 | ("Configuration", "Release"); 139 | ("DefineConstants", "MONO") 140 | ]) 141 | |> ignore 142 | ) 143 | #else 144 | traceImportant "WIN" 145 | 146 | Target "Build" (fun _ -> 147 | 148 | !! solutionFile 149 | //|> MSBuildRelease "" "Rebuild" 150 | |> MSBuild "" "Rebuild" ([ 151 | ("Configuration", "Release"); 152 | ("DefineConstants", "WIN") 153 | ]) 154 | |> ignore 155 | ) 156 | #endif 157 | 158 | // -------------------------------------------------------------------------------------- 159 | // Run the unit tests using test runner 160 | 161 | Target "RunTests" (fun _ -> 162 | !! testAssemblies 163 | |> NUnit (fun p -> 164 | { p with 165 | DisableShadowCopy = true 166 | TimeOut = TimeSpan.FromMinutes 20. 167 | OutputFile = "TestResults.xml" }) 168 | ) 169 | 170 | #if MONO 171 | #else 172 | // -------------------------------------------------------------------------------------- 173 | // SourceLink allows Source Indexing on the PDB generated by the compiler, this allows 174 | // the ability to step through the source code of external libraries https://github.com/ctaggart/SourceLink 175 | 176 | //Target "SourceLink" (fun _ -> 177 | // let baseUrl = sprintf "%s/%s/{0}/%%var2%%" gitRaw (project.ToLower()) 178 | // use repo = new GitRepo(__SOURCE_DIRECTORY__) 179 | 180 | // let addAssemblyInfo (projFileName:String) = 181 | // match projFileName with 182 | // | Fsproj -> (projFileName, "**/AssemblyInfo.fs") 183 | // | Csproj -> (projFileName, "**/AssemblyInfo.cs") 184 | // | Vbproj -> (projFileName, "**/AssemblyInfo.vb") 185 | 186 | // !! "src/**/*.??proj" 187 | // |> Seq.map addAssemblyInfo 188 | // |> Seq.iter (fun (projFile, assemblyInfo) -> 189 | // let proj = VsProj.LoadRelease projFile 190 | // logfn "source linking %s" proj.OutputFilePdb 191 | // let files = proj.CompilesNotLinked -- assemblyInfo 192 | // repo.VerifyChecksums files 193 | // proj.VerifyPdbChecksums files 194 | // proj.CreateSrcSrv baseUrl repo.Commit (repo.Paths files) 195 | // Pdbstr.exec proj.OutputFilePdb proj.OutputFilePdbSrcSrv 196 | // ) 197 | //) 198 | 199 | #endif 200 | 201 | // -------------------------------------------------------------------------------------- 202 | // Build a NuGet package 203 | 204 | Target "NuGet" (fun _ -> 205 | Paket.Pack(fun p -> 206 | { p with 207 | OutputPath = "bin" 208 | Version = release.NugetVersion 209 | ReleaseNotes = toLines release.Notes}) 210 | ) 211 | 212 | Target "PublishNuget" (fun _ -> 213 | Paket.Push(fun p -> 214 | { p with 215 | WorkingDir = "bin" }) 216 | ) 217 | 218 | 219 | // -------------------------------------------------------------------------------------- 220 | // Generate the documentation 221 | 222 | Target "GenerateReferenceDocs" (fun _ -> 223 | if not <| executeFSIWithArgs "docs/tools" "generate.fsx" ["--define:RELEASE"; "--define:REFERENCE"] [] then 224 | failwith "generating reference documentation failed" 225 | ) 226 | 227 | let generateHelp' fail debug = 228 | let args = 229 | if debug then ["--define:HELP"] 230 | else ["--define:RELEASE"; "--define:HELP"] 231 | if executeFSIWithArgs "docs/tools" "generate.fsx" args [] then 232 | traceImportant "Help generated" 233 | else 234 | if fail then 235 | failwith "generating help documentation failed" 236 | else 237 | traceImportant "generating help documentation failed" 238 | 239 | let generateHelp fail = 240 | generateHelp' fail false 241 | 242 | Target "GenerateHelp" (fun _ -> 243 | DeleteFile "docs/content/release-notes.md" 244 | CopyFile "docs/content/" "RELEASE_NOTES.md" 245 | Rename "docs/content/release-notes.md" "docs/content/RELEASE_NOTES.md" 246 | 247 | DeleteFile "docs/content/license.md" 248 | CopyFile "docs/content/" "LICENSE.txt" 249 | Rename "docs/content/license.md" "docs/content/LICENSE.txt" 250 | 251 | generateHelp true 252 | ) 253 | 254 | Target "GenerateHelpDebug" (fun _ -> 255 | DeleteFile "docs/content/release-notes.md" 256 | CopyFile "docs/content/" "RELEASE_NOTES.md" 257 | Rename "docs/content/release-notes.md" "docs/content/RELEASE_NOTES.md" 258 | 259 | DeleteFile "docs/content/license.md" 260 | CopyFile "docs/content/" "LICENSE.txt" 261 | Rename "docs/content/license.md" "docs/content/LICENSE.txt" 262 | 263 | generateHelp' true true 264 | ) 265 | 266 | Target "KeepRunning" (fun _ -> 267 | use watcher = new FileSystemWatcher(DirectoryInfo("docs/content").FullName,"*.*") 268 | watcher.EnableRaisingEvents <- true 269 | watcher.Changed.Add(fun e -> generateHelp false) 270 | watcher.Created.Add(fun e -> generateHelp false) 271 | watcher.Renamed.Add(fun e -> generateHelp false) 272 | watcher.Deleted.Add(fun e -> generateHelp false) 273 | 274 | traceImportant "Waiting for help edits. Press any key to stop." 275 | 276 | System.Console.ReadKey() |> ignore 277 | 278 | watcher.EnableRaisingEvents <- false 279 | watcher.Dispose() 280 | ) 281 | 282 | Target "GenerateDocs" DoNothing 283 | 284 | let createIndexFsx lang = 285 | let content = """(*** hide ***) 286 | // This block of code is omitted in the generated HTML documentation. Use 287 | // it to define helpers that you do not want to show in the documentation. 288 | #I "../../../bin" 289 | 290 | (** 291 | F# Project Scaffold ({0}) 292 | ========================= 293 | *) 294 | """ 295 | let targetDir = "docs/content" @@ lang 296 | let targetFile = targetDir @@ "index.fsx" 297 | ensureDirectory targetDir 298 | System.IO.File.WriteAllText(targetFile, System.String.Format(content, lang)) 299 | 300 | Target "AddLangDocs" (fun _ -> 301 | let args = System.Environment.GetCommandLineArgs() 302 | if args.Length < 4 then 303 | failwith "Language not specified." 304 | 305 | args.[3..] 306 | |> Seq.iter (fun lang -> 307 | if lang.Length <> 2 && lang.Length <> 3 then 308 | failwithf "Language must be 2 or 3 characters (ex. 'de', 'fr', 'ja', 'gsw', etc.): %s" lang 309 | 310 | let templateFileName = "template.cshtml" 311 | let templateDir = "docs/tools/templates" 312 | let langTemplateDir = templateDir @@ lang 313 | let langTemplateFileName = langTemplateDir @@ templateFileName 314 | 315 | if System.IO.File.Exists(langTemplateFileName) then 316 | failwithf "Documents for specified language '%s' have already been added." lang 317 | 318 | ensureDirectory langTemplateDir 319 | Copy langTemplateDir [ templateDir @@ templateFileName ] 320 | 321 | createIndexFsx lang) 322 | ) 323 | 324 | // -------------------------------------------------------------------------------------- 325 | // Release Scripts 326 | 327 | //Target "ReleaseDocs" (fun _ -> 328 | // let tempDocsDir = "temp/gh-pages" 329 | // CleanDir tempDocsDir 330 | // Repository.cloneSingleBranch "" (gitHome + "/" + gitName + ".git") "gh-pages" tempDocsDir 331 | 332 | // CopyRecursive "docs/output" tempDocsDir true |> tracefn "%A" 333 | // StageAll tempDocsDir 334 | // Git.Commit.Commit tempDocsDir (sprintf "Update generated documentation for version %s" release.NugetVersion) 335 | // Branches.push tempDocsDir 336 | //) 337 | 338 | //#load "paket-files/fsharp/FAKE/modules/Octokit/Octokit.fsx" 339 | //open Octokit 340 | 341 | Target "Release" DoNothing// (fun _ -> 342 | // StageAll "" 343 | // Git.Commit.Commit "" (sprintf "Bump version to %s" release.NugetVersion) 344 | // Branches.push "" 345 | 346 | // Branches.tag "" release.NugetVersion 347 | // Branches.pushTag "" "origin" release.NugetVersion 348 | 349 | // // release on github 350 | // createClient (getBuildParamOrDefault "github-user" "") (getBuildParamOrDefault "github-pw" "") 351 | // |> createDraft gitOwner gitName release.NugetVersion (release.SemVer.PreRelease <> None) release.Notes 352 | // // TODO: |> uploadFile "PATH_TO_FILE" 353 | // |> releaseDraft 354 | // |> Async.RunSynchronously 355 | //) 356 | 357 | Target "BuildPackage" DoNothing 358 | 359 | // -------------------------------------------------------------------------------------- 360 | // Run all targets by default. Invoke 'build ' to override 361 | 362 | Target "All" DoNothing 363 | Target "Default" DoNothing 364 | 365 | "Clean" 366 | ==> "AssemblyInfo" 367 | ==> "Build" 368 | ==> "CopyBinaries" 369 | ==> "RunTests" 370 | // ==> "GenerateReferenceDocs" https://github.com/fsprojects/ProjectScaffold/issues/170 371 | // ==> "GenerateDocs" 372 | //==> "All" 373 | //=?> ("ReleaseDocs",isLocalBuild) 374 | 375 | "All" 376 | #if MONO 377 | #else 378 | //=?> ("SourceLink", Pdbstr.tryFind().IsSome ) 379 | #endif 380 | //==> "NuGet" 381 | //==> "BuildPackage" 382 | ==> "Build" 383 | 384 | //"CleanDocs" 385 | // ==> "GenerateHelp" 386 | //// ==> "GenerateReferenceDocs" https://github.com/fsprojects/ProjectScaffold/issues/170 387 | //// ==> "GenerateDocs" 388 | 389 | //"CleanDocs" 390 | // ==> "GenerateHelpDebug" 391 | 392 | //"GenerateHelp" 393 | // ==> "KeepRunning" 394 | 395 | //"ReleaseDocs" 396 | // ==> "Release" 397 | 398 | //"BuildPackage" 399 | // ==> "PublishNuget" 400 | // ==> "Release" 401 | 402 | "Default" 403 | ==> "Build" 404 | 405 | RunTargetOrDefault "Build" 406 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | cd `dirname $0` 7 | 8 | FSIARGS="" 9 | OS=${OS:-"unknown"} 10 | if [[ "$OS" != "Windows_NT" ]] 11 | then 12 | FSIARGS="--fsiargs -d:MONO" 13 | fi 14 | 15 | function run() { 16 | if [[ "$OS" != "Windows_NT" ]] 17 | then 18 | mono "$@" 19 | else 20 | "$@" 21 | fi 22 | } 23 | 24 | run .paket/paket.bootstrapper.exe 25 | 26 | if [[ "$OS" != "Windows_NT" ]] && 27 | [ ! -e ~/.config/.mono/certs ] 28 | then 29 | mozroots --import --sync --quiet 30 | fi 31 | 32 | run .paket/paket.exe restore 33 | 34 | [ ! -e build.fsx ] && run .paket/paket.exe update 35 | [ ! -e build.fsx ] && run packages/FAKE/tools/FAKE.exe init.fsx 36 | run packages/FAKE/tools/FAKE.exe "$@" $FSIARGS build.fsx -------------------------------------------------------------------------------- /misc/README.md: -------------------------------------------------------------------------------- 1 | ```fsharp 2 | open System.Diagnostics 3 | open System 4 | open FsMenu.Core 5 | 6 | let paketPath = System.IO.Path.Combine("..", "paket.exe") 7 | 8 | let paket args = 9 | let p = new Process() 10 | p.StartInfo.FileName <- paketPath 11 | p.StartInfo.CreateNoWindow <- false 12 | p.StartInfo.Arguments <- args 13 | p.StartInfo.UseShellExecute <- false 14 | p.StartInfo.RedirectStandardOutput <- true 15 | p.Start() |> ignore 16 | 17 | while not p.StandardOutput.EndOfStream do 18 | printfn "%s" (p.StandardOutput.ReadLine()) 19 | 20 | 21 | let paketAddSubMenu = 22 | let mutable add = "add" 23 | let mutable name = "" 24 | let mutable force = "" 25 | let mutable verbose = "" 26 | // ... 27 | 28 | let yesNoMenu onYes onNo = 29 | Menu [ 30 | "yes" <+ onYes 31 | "no " <+ onNo] 32 | 33 | let setName() = printf "name: " 34 | name <- Console.ReadLine() 35 | 36 | let setForceMenu = 37 | yesNoMenu (fun () -> force <- "--force") (fun () -> force <- "") 38 | 39 | let setVerboseMenu = 40 | yesNoMenu (fun () -> verbose <- "--verbose") (fun () -> verbose <- "") 41 | 42 | let runAddWithArgs() = 43 | [name 44 | force 45 | verbose] 46 | |> List.filter (fun arg -> arg <> "") 47 | |> List.fold (fun acc arg -> sprintf "%s %s" acc arg) add 48 | |> printf "%s" 49 | 50 | 51 | let men = 52 | Menu [ 53 | "name " <+= setName 54 | "force " +> setForceMenu 55 | "verbose" +> setVerboseMenu 56 | "run_add" => runAddWithArgs] 57 | men 58 | 59 | [] 60 | let main argv = 61 | 62 | printfn "" 63 | 64 | let test = 65 | Menu [ 66 | "install " => (fun () -> paket "install" |> ignore) 67 | "update " => (fun () -> paket "update" |> ignore) 68 | "restore " => (fun () -> paket "restore" |> ignore) 69 | "add package" +> paketAddSubMenu ] 70 | 71 | render test "|> exec" 72 | 73 | 74 | Console.ReadLine() |> ignore 75 | 0 76 | ``` 77 | 78 | ![](https://github.com/nicolaiw/FsMenu/blob/master/misc/sample2.gif) -------------------------------------------------------------------------------- /misc/sample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolaiw/FsMenu/4f52cf9747675d15e1e0994b5775227d9e708475/misc/sample.gif -------------------------------------------------------------------------------- /misc/sample2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolaiw/FsMenu/4f52cf9747675d15e1e0994b5775227d9e708475/misc/sample2.gif -------------------------------------------------------------------------------- /misc/sample3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolaiw/FsMenu/4f52cf9747675d15e1e0994b5775227d9e708475/misc/sample3.gif -------------------------------------------------------------------------------- /misc/sample4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolaiw/FsMenu/4f52cf9747675d15e1e0994b5775227d9e708475/misc/sample4.gif -------------------------------------------------------------------------------- /misc/sample5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolaiw/FsMenu/4f52cf9747675d15e1e0994b5775227d9e708475/misc/sample5.gif -------------------------------------------------------------------------------- /paket.dependencies: -------------------------------------------------------------------------------- 1 | source https://www.nuget.org/api/v2 2 | nuget Fake 3 | nuget System.ValueTuple 4.3.0 restriction: >= net452 -------------------------------------------------------------------------------- /paket.lock: -------------------------------------------------------------------------------- 1 | NUGET 2 | remote: https://www.nuget.org/api/v2 3 | FAKE (4.62.6) 4 | System.ValueTuple (4.3) - restriction: >= net452 5 | -------------------------------------------------------------------------------- /src/FsMenu.Core/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | // Auto-Generated by FAKE; do not edit 2 | namespace System 3 | open System.Reflection 4 | 5 | [] 6 | [] 7 | [] 8 | [] 9 | [] 10 | do () 11 | 12 | module internal AssemblyVersionInformation = 13 | let [] AssemblyTitle = "FsMenu.Core" 14 | let [] AssemblyProduct = "FsMenu" 15 | let [] AssemblyDescription = "Small DSL to create an interactive cli" 16 | let [] AssemblyVersion = "0.3.0" 17 | let [] AssemblyFileVersion = "0.3.0" 18 | -------------------------------------------------------------------------------- /src/FsMenu.Core/Colors.fs: -------------------------------------------------------------------------------- 1 | namespace FsMenu 2 | 3 | 4 | module Colors = 5 | 6 | type Color = 7 | | None = -1 8 | | Black = 0 9 | | DarkBlue = 1 10 | | DarkGreen = 2 11 | | DarkCyan = 3 12 | | DarkRed = 4 13 | | DarkMagenta = 5 14 | | DarkYellow = 6 15 | | Gray = 7 16 | | DarkGray = 8 17 | | Blue = 9 18 | | Green = 10 19 | | Cyan = 11 20 | | Red = 12 21 | | Magenta = 13 22 | | Yellow = 14 23 | | White = 15 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/FsMenu.Core/FsMenu.Core.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | 2.0 8 | b1e153e6-8a13-4d07-81f5-161fd571c1be 9 | Library 10 | FsMenu.Core 11 | FsMenu.Core 12 | v4.5.2 13 | 4.4.1.0 14 | true 15 | FsMenu.Core 16 | 17 | 18 | true 19 | full 20 | false 21 | false 22 | bin\$(Configuration)\ 23 | DEBUG;TRACE 24 | 3 25 | bin\$(Configuration)\$(AssemblyName).XML 26 | 27 | 28 | pdbonly 29 | true 30 | true 31 | bin\$(Configuration)\ 32 | TRACE 33 | 3 34 | bin\$(Configuration)\$(AssemblyName).XML 35 | 36 | 37 | 11 38 | 39 | 40 | 41 | 42 | $(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets 43 | 44 | 45 | 46 | 47 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | FSharp.Core 62 | FSharp.Core.dll 63 | $(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\FSharp\.NETFramework\v4.0\$(TargetFSharpCoreVersion)\FSharp.Core.dll 64 | 65 | 66 | 67 | 68 | 69 | 76 | 77 | 78 | 79 | 80 | 81 | ..\..\packages\System.ValueTuple\lib\netstandard1.0\System.ValueTuple.dll 82 | True 83 | True 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/FsMenu.Core/FsMenu.fs: -------------------------------------------------------------------------------- 1 |  2 | module FsMenu.Core 3 | 4 | type AfterExecution = 5 | | NavigateBack 6 | | Stay 7 | | Exit 8 | 9 | type MenuEntry = 10 | | Action of (unit -> AfterExecution) 11 | | Sub of (string * MenuEntry) list 12 | 13 | open System 14 | open FsMenu.Colors 15 | 16 | (* Small DSL *) 17 | let Menu = Sub 18 | 19 | // Render sub menu 20 | let (+>) name entry : (string * MenuEntry) = (name,entry) 21 | 22 | // Execute action and exit 23 | let (=>) s f = (s, Action (fun () -> f(); Exit)) 24 | 25 | // Execute action and render the previous menu 26 | let (<+) s f = (s, Action (fun () -> f(); NavigateBack)) 27 | 28 | // Execute action and render the menu where you come frome 29 | let (<+=) s f = (s, Action (fun () -> f(); Stay)) 30 | 31 | 32 | // TODO: 33 | // 1. create Nuget package 34 | 35 | 36 | type private Colorize = 37 | | Emphasizer 38 | | Entry 39 | | Both 40 | 41 | (* Colorizes the emphasizer with a given color *) 42 | let private colorizeEmphasizer (oldIndex: int) (newIndex:int) (emphasizer: string) (color: Color) (entries: string list) = 43 | 44 | let inititalCursorPos = Console.CursorTop 45 | 46 | (* Clear the current emphasizer *) 47 | Console.SetCursorPosition(entries.[oldIndex].Length + 1, Console.CursorTop - (entries.Length-oldIndex)) 48 | Console.Write(new string (' ', emphasizer.Length)) 49 | 50 | (* Emphasise the new entry *) 51 | Console.SetCursorPosition(0, inititalCursorPos) 52 | Console.SetCursorPosition(entries.[newIndex].Length, Console.CursorTop - (entries.Length-newIndex)) 53 | 54 | match color with 55 | | Color.None -> 56 | Console.Write(sprintf " %s" emphasizer) 57 | 58 | | c as consoleColor -> 59 | let currentForegroundColor = Console.ForegroundColor 60 | Console.ForegroundColor <- enum (unbox c) 61 | Console.Write(sprintf " %s" emphasizer) 62 | Console.ForegroundColor <- currentForegroundColor 63 | 64 | (* Reset the Cursor *) 65 | Console.SetCursorPosition(0, inititalCursorPos); 66 | 67 | 68 | (* Colorizes an entry *) 69 | let private colorizeEntry (oldIndex: int) (newIndex:int) (color: Color) (entries: string list) = 70 | 71 | let inititalCursorPos = Console.CursorTop 72 | 73 | (* Clear the current entry *) 74 | Console.SetCursorPosition(0, Console.CursorTop - (entries.Length-oldIndex)) 75 | Console.Write(entries.[oldIndex]) 76 | 77 | (* Emphasise the new entry *) 78 | Console.SetCursorPosition(0, inititalCursorPos) 79 | 80 | let currentForegroundColor = Console.ForegroundColor 81 | 82 | match color with 83 | | Color.None -> () 84 | 85 | | c as consoleColor -> 86 | 87 | Console.ForegroundColor <- enum (unbox c) 88 | Console.SetCursorPosition(0, Console.CursorTop - (entries.Length-newIndex)) 89 | Console.Write(sprintf "%s" entries.[newIndex]) 90 | Console.ForegroundColor <- currentForegroundColor 91 | 92 | (* Reset the Cursor *) 93 | Console.SetCursorPosition(0, inititalCursorPos); 94 | 95 | (* Colorizes the whole line including the emphasizer *) 96 | let private colorizeLine (oldIndex: int) (newIndex:int) (emphasizer: string) (color: Color) (entries: string list) = 97 | 98 | // TODO: explicite colorization instead of calling both functions 99 | colorizeEntry oldIndex newIndex color entries 100 | colorizeEmphasizer oldIndex newIndex emphasizer color entries 101 | 102 | 103 | (* Emphasizes an entry *) 104 | let private emphasizeEntry (oldIndex: int) (newIndex:int) (emphasizer: string) (color: Color) (colorize: Colorize) (entries: string list) = 105 | 106 | match colorize with 107 | | Emphasizer -> colorizeEmphasizer oldIndex newIndex emphasizer color entries 108 | | Entry -> colorizeEntry oldIndex newIndex color entries 109 | | Both -> colorizeLine oldIndex newIndex emphasizer color entries 110 | 111 | 112 | (* Clears the current menu *) 113 | let private clear entryCount = 114 | let diff = Math.Abs (Console.CursorTop - entryCount) 115 | 116 | for i=0 to entryCount do 117 | Console.SetCursorPosition(0, i + diff); 118 | Console.Write(new string(' ', Console.WindowWidth)) 119 | 120 | Console.SetCursorPosition(0, diff); 121 | 122 | 123 | (* Shows the menu and handles Input *) 124 | let private renderColored menuEntry emphesizer color colorize= 125 | 126 | (* All recursive stuff is hidden for the user of "render" *) 127 | let rec renderSubMenuRec callStack menuEntry emphasize = 128 | match menuEntry with 129 | | Sub (subMenu) -> 130 | for i = 0 to subMenu.Length-1 do 131 | let name = fst subMenu.[i] 132 | //let entry = if i = emphasize then sprintf "%s %s" name emphesizer else name 133 | printfn "%s" name; 134 | 135 | subMenu 136 | |> List.map fst 137 | |> emphasizeEntry 0 0 emphesizer color colorize 138 | 139 | let rec handleUserInput currentEntry = 140 | let cki = Console.ReadKey(true) 141 | 142 | match cki.Key with 143 | | ConsoleKey.DownArrow -> 144 | if currentEntry+1 < subMenu.Length then 145 | subMenu |> List.map fst |> emphasizeEntry currentEntry (currentEntry+1) emphesizer color colorize 146 | handleUserInput (currentEntry+1) 147 | else 148 | handleUserInput currentEntry 149 | 150 | | ConsoleKey.UpArrow -> 151 | if currentEntry-1 >= 0 then 152 | subMenu |> List.map fst |> emphasizeEntry currentEntry (currentEntry-1) emphesizer color colorize 153 | handleUserInput (currentEntry-1) 154 | else 155 | handleUserInput currentEntry 156 | 157 | | ConsoleKey.Enter -> 158 | (* Either call the action or navigate downwards *) 159 | match snd subMenu.[currentEntry] with 160 | | Action handler -> 161 | let cursorPosBeforeExecute = Console.CursorTop 162 | 163 | match handler() with 164 | | NavigateBack -> 165 | (* If the user does some prints we have to delete those lines as well *) 166 | let cursorDiff = Console.CursorTop-cursorPosBeforeExecute 167 | clear (subMenu.Length+cursorDiff) 168 | 169 | let (previousEntry, stack) = 170 | match callStack with 171 | | [] -> (menuEntry, callStack) 172 | | hd::tail -> (hd,tail) 173 | 174 | renderSubMenuRec stack previousEntry emphasize 175 | 176 | | Stay -> 177 | (* If the user does some prints we have to delete those lines as well *) 178 | let cursorDiff = Console.CursorTop-cursorPosBeforeExecute 179 | clear (subMenu.Length+cursorDiff) 180 | renderSubMenuRec callStack menuEntry emphasize 181 | 182 | | Exit -> () 183 | 184 | | Sub sub -> 185 | clear subMenu.Length 186 | renderSubMenuRec (menuEntry::callStack) (snd subMenu.[currentEntry]) 0 187 | 188 | | ConsoleKey.Backspace -> 189 | match callStack with 190 | | [] -> 191 | handleUserInput currentEntry 192 | 193 | | hd::t -> // hd contains the previous 194 | clear subMenu.Length 195 | renderSubMenuRec t hd 0 196 | 197 | | _ -> handleUserInput currentEntry 198 | 199 | handleUserInput emphasize 200 | 201 | | _ -> () 202 | 203 | renderSubMenuRec [] menuEntry 0 204 | 205 | 206 | let render menuEntry emphesizer = 207 | renderColored menuEntry emphesizer Color.None Colorize.Emphasizer 208 | 209 | let renderWithColoredEmphaziser menuEntry emphaziser color = 210 | renderColored menuEntry emphaziser color Colorize.Emphasizer 211 | 212 | let renderWithColoredEntry menuEntry color = 213 | renderColored menuEntry System.String.Empty color Colorize.Entry 214 | 215 | let renderWithColoredLine menuEntry emphesizer color = 216 | renderColored menuEntry emphesizer color Colorize.Both 217 | -------------------------------------------------------------------------------- /src/FsMenu.Core/paket.references: -------------------------------------------------------------------------------- 1 | System.ValueTuple -------------------------------------------------------------------------------- /tests/FsMenu.Console/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/FsMenu.Console/FsMenu.Console.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | 2.0 8 | ea938605-9ecd-45e1-bfef-8f6b1f5aefa4 9 | Exe 10 | FsMenu 11 | FsMenu.Console 12 | v4.5.2 13 | true 14 | 4.4.1.0 15 | FsMenu.Console 16 | 17 | 18 | true 19 | full 20 | false 21 | false 22 | bin\$(Configuration)\ 23 | DEBUG;TRACE 24 | 3 25 | AnyCPU 26 | bin\$(Configuration)\$(AssemblyName).XML 27 | true 28 | 29 | 30 | pdbonly 31 | true 32 | true 33 | bin\$(Configuration)\ 34 | TRACE 35 | 3 36 | AnyCPU 37 | bin\$(Configuration)\$(AssemblyName).XML 38 | true 39 | 40 | 41 | 11 42 | 43 | 44 | 45 | 46 | $(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets 47 | 48 | 49 | 50 | 51 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | AssemblyInfo.fs 61 | 62 | 63 | 64 | 65 | 66 | 67 | FSharp.Core 68 | FSharp.Core.dll 69 | $(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\FSharp\.NETFramework\v4.0\$(TargetFSharpCoreVersion)\FSharp.Core.dll 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | FsMenu.Core 78 | {b1e153e6-8a13-4d07-81f5-161fd571c1be} 79 | True 80 | 81 | 82 | 89 | 90 | 91 | 92 | 93 | 94 | ..\..\packages\System.ValueTuple\lib\netstandard1.0\System.ValueTuple.dll 95 | True 96 | True 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /tests/FsMenu.Console/Program.fs: -------------------------------------------------------------------------------- 1 | open System 2 | open FsMenu.Core 3 | open FsMenu.Colors 4 | 5 | 6 | let testFunc() = 7 | printfn "selected Sub Sub 3" 8 | printf "handle some input: " 9 | let input = Console.ReadLine() 10 | // Do some stuff with input 11 | () 12 | 13 | 14 | [] 15 | let main argv = 16 | 17 | (* 18 | The ugly way 19 | don't use it 20 | *) 21 | 22 | let mutable yesOrNoUglyWay = "" 23 | 24 | let uglyTest = 25 | Sub ([ 26 | ("Item 1", MenuEntry.Action (fun () -> printf "selected Item 1"; Exit)) 27 | ("Item 2", 28 | Sub ([ 29 | ("Sub 1", 30 | Sub ([ 31 | ("Sub Sub 1", MenuEntry.Action (fun () -> printf "selected Sub Sub 1"; Exit)) 32 | ("Sub Sub 2", 33 | Sub ([ 34 | ("yes", Action (fun () -> yesOrNoUglyWay <- "--yes"; NavigateBack)) 35 | ("no ", Action (fun () -> yesOrNoUglyWay <- "--no"; NavigateBack)) 36 | ])) 37 | ("Sub Sub 3", MenuEntry.Action (fun () -> testFunc(); NavigateBack)) 38 | ])) 39 | ("Sub 2", MenuEntry.Action (fun () -> printf "selected Sub 2"; Exit)) 40 | ("Sub 3", MenuEntry.Action (fun () -> printf "exec some command with param %s" yesOrNoUglyWay; Exit)) 41 | ])) 42 | ]) 43 | 44 | 45 | printfn "Use Keys: UP-Arrow DOWN-Arrow ENTER BACK-SPACE\n" 46 | 47 | (* 48 | The same using a small DSL 49 | *) 50 | 51 | let mutable yesOrNo = "" 52 | 53 | let test = 54 | Menu [ 55 | "Item 1" => (fun () -> printf "selected Item 1") 56 | "Item 2" +> 57 | Menu [ 58 | "Sub 1" +> 59 | Menu [ 60 | "Sub Sub 1" => (fun () -> printf "selected Sub Sub 1") 61 | "Sub Sub 2" +> 62 | Menu [ 63 | "yes" <+ (fun () -> yesOrNo <- "--yes") 64 | "no " <+ (fun () -> yesOrNo <- "--no") ] 65 | "Sub Sub 3" <+ testFunc] 66 | "Sub 2" => (fun () -> printf "selected Sub 2") 67 | "Sub 3" => (fun () -> printf "exec some command with param %s" yesOrNo)]] 68 | 69 | 70 | //render test "<--" 71 | //renderWithColoredEmphaziser test "<--" Color.Green 72 | //renderWithColoredEntry test Color.Cyan 73 | renderWithColoredLine test "<--" Color.Green 74 | 75 | 76 | Console.ReadLine() |> ignore 77 | 0 78 | -------------------------------------------------------------------------------- /tests/FsMenu.Console/paket.references: -------------------------------------------------------------------------------- 1 | System.ValueTuple --------------------------------------------------------------------------------