diff --git a/cells/std/Readme.md b/cells/std/Readme.md new file mode 100644 index 00000000..57acef93 --- /dev/null +++ b/cells/std/Readme.md @@ -0,0 +1 @@ +# `std` Cell diff --git a/cells/std/cli.nix b/cells/std/cli.nix new file mode 100644 index 00000000..8ca99145 --- /dev/null +++ b/cells/std/cli.nix @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2022 The Standard Authors +# SPDX-FileCopyrightText: 2022 Kevin Amado +{ + inputs, + cell, +}: let + nixpkgs = inputs.nixpkgs; +in { + default = let + commit = inputs.self.shortRev or "dirty"; + date = inputs.self.lastModifiedDate or inputs.self.lastModified or "19700101"; + version = "0.1.0+${builtins.substring 0 8 date}.${commit}"; + in + nixpkgs.buildGoModule rec { + inherit version; + pname = "std"; + meta.description = "A tui for projects that conform to Standard"; + + src = ./cli; + + vendorSha256 = null; + + ldflags = [ + "-s" + "-w" + "-X main.buildVersion=${version}" + "-X main.buildCommit=${commit}" + ]; + }; +} diff --git a/cells/std/cli/.gitignore b/cells/std/cli/.gitignore new file mode 100644 index 00000000..22d0d82f --- /dev/null +++ b/cells/std/cli/.gitignore @@ -0,0 +1 @@ +vendor diff --git a/cells/std/cli/Readme.md b/cells/std/cli/Readme.md new file mode 100644 index 00000000..f7a6e40c --- /dev/null +++ b/cells/std/cli/Readme.md @@ -0,0 +1 @@ +# `cli` Readme diff --git a/cells/std/cli/data/data.go b/cells/std/cli/data/data.go new file mode 100644 index 00000000..a7f7433b --- /dev/null +++ b/cells/std/cli/data/data.go @@ -0,0 +1,47 @@ +package data + +import ( + "fmt" + + "github.com/charmbracelet/bubbles/list" +) + +const targetTemplate = "//%s/%s:%s" + +type Item struct { + StdName string `json:"__std_name"` + StdOrganelle string `json:"__std_organelle"` + StdCell string `json:"__std_cell"` + StdClade string `json:"__std_clade"` + StdDescription string `json:"__std_description"` + StdCellReadme string `json:"__std_cell_readme"` + StdOrganelleReadme string `json:"__std_organelle_readme"` + StdReadme string `json:"__std_readme"` + StdCladeActions []Action `json:"__std_actions"` +} + +func (i Item) Title() string { + return fmt.Sprintf(targetTemplate, i.StdCell, i.StdOrganelle, i.StdName) +} +func (i Item) Description() string { return i.StdDescription } +func (i Item) FilterValue() string { return i.Title() } + +func (i Item) GetActionItems() []list.Item { + var numItems = len(i.StdCladeActions) + // Make list of actions + items := make([]list.Item, numItems) + for j := 0; j < numItems; j++ { + items[j] = i.StdCladeActions[j] + } + return items +} + +type Action struct { + ActionName string `json:"name"` + ActionCommand []string `json:"command"` + ActionDescription string `json:"description"` +} + +func (a Action) Title() string { return a.ActionName } +func (a Action) Description() string { return a.ActionDescription } +func (a Action) FilterValue() string { return a.Title() } diff --git a/cells/std/cli/default.md b/cells/std/cli/default.md new file mode 100644 index 00000000..6411aa87 --- /dev/null +++ b/cells/std/cli/default.md @@ -0,0 +1 @@ +# `std` Readme diff --git a/cells/std/cli/default.nix b/cells/std/cli/default.nix deleted file mode 100644 index b2228322..00000000 --- a/cells/std/cli/default.nix +++ /dev/null @@ -1,113 +0,0 @@ -# SPDX-FileCopyrightText: 2022 The Standard Authors -# SPDX-FileCopyrightText: 2022 Kevin Amado -{ - inputs, - cell, -}: let - nixpkgs = inputs.nixpkgs; -in { - default = let - inherit (nixpkgs.chickenPackages) chickenEggs; - shell = nixpkgs.eggDerivation { - name = "shell-0.4"; - src = nixpkgs.fetchegg { - name = "shell"; - version = "0.4"; - sha256 = "sha256-TVZBzvlVegDLNU3Nz3w92E7imXGw6HYOq+vm2amM+/w="; - }; - buildInputs = []; - }; - lazy-seq = nixpkgs.eggDerivation { - name = "lazy-seq-2"; - src = nixpkgs.fetchegg { - name = "lazy-seq"; - version = "2"; - sha256 = "sha256-emjinMqdMpeaU9O7qN4v/xiJGfZyHC9YTMQlkk/aoB0="; - }; - buildInputs = with chickenEggs; [srfi-1]; - }; - trie = nixpkgs.eggDerivation { - name = "trie-2"; - src = nixpkgs.fetchegg { - name = "trie"; - version = "2"; - sha256 = "sha256-kdsywnZqJVFO+2vCba5BbpLW4azODO70LhgqQWzn65I="; - }; - buildInputs = with chickenEggs; [srfi-1]; - }; - srfi-69 = nixpkgs.eggDerivation { - name = "srfi-69-0.4.3"; - src = nixpkgs.fetchegg { - name = "srfi-69"; - version = "0.4.3"; - sha256 = "sha256-ejqj0EghSr7Ac4/fD5AHiRgbrmpJd9bVrTcPZknx9oY="; - # sha256 = nixpkgs.lib.fakeSha256; - }; - }; - comparse = nixpkgs.eggDerivation { - name = "comparse-3"; - src = nixpkgs.fetchegg { - name = "comparse"; - version = "3"; - sha256 = "sha256-gpGLRF1cTvkEjEhV95jwaPpGI6tXoP2WqwSCZILShnU="; - }; - buildInputs = with chickenEggs; [lazy-seq trie matchable srfi-13 srfi-69]; - }; - medea = nixpkgs.eggDerivation { - name = "medea-4"; - src = nixpkgs.fetchegg { - name = "medea"; - version = "4"; - sha256 = "sha256-29AZOHuyacFeNS1dmH9qOxDB0IeyM8Lz0z1JQitdKOc="; - }; - buildInputs = [comparse]; - }; - iset = nixpkgs.eggDerivation { - name = "iset"; - src = nixpkgs.fetchegg { - name = "iset"; - version = "2.2"; - sha256 = "sha256-P49qdCSmd/5OFVe2DcgkhW3QA/Jyr+Wwtd1wdkBj03k="; - }; - }; - regex = nixpkgs.eggDerivation { - name = "regex"; - src = nixpkgs.fetchegg { - name = "regex"; - version = "2.0"; - sha256 = "sha256-kkgEI2EA/XFTlyxdF9zG/EdoRzJr+474e9iXlFvO+GE="; - }; - }; - utf8 = nixpkgs.eggDerivation { - name = "utf8"; - src = nixpkgs.fetchegg { - name = "utf8"; - version = "3.6.3"; - sha256 = "sha256-PJ53wuNStzziNhrG9Uu14Dc3mcPuncgodZM/Zxdtgbw="; - }; - buildInputs = with chickenEggs; [iset srfi-69 regex]; - }; - fmt = nixpkgs.eggDerivation { - name = "fmt"; - src = nixpkgs.fetchegg { - name = "fmt"; - version = "0.8.11"; - sha256 = "sha256-nmDj5Xmo29V+nLnxsIz5/bD72LtyyYWLNU4/CmFaGVg="; - }; - buildInputs = with chickenEggs; [srfi-1 srfi-13 srfi-69 utf8]; - }; - in - nixpkgs.stdenv.mkDerivation { - name = "std"; - meta.description = "nix shortcut for projects that conform to Standard"; - src = ./.; - dontInstall = true; - nativeBuildInputs = [nixpkgs.chicken]; - buildInputs = with nixpkgs.chickenPackages.chickenEggs; [matchable srfi-13 shell medea fmt]; - propagatedBuildInputs = [nixpkgs.git]; - buildPhase = '' - mkdir -p $out/bin - csc -o $out/bin/std -static "$src/main.scm" - ''; - }; -} diff --git a/cells/std/cli/dummy_data/random-readme-1.md b/cells/std/cli/dummy_data/random-readme-1.md new file mode 100644 index 00000000..95c9c999 --- /dev/null +++ b/cells/std/cli/dummy_data/random-readme-1.md @@ -0,0 +1,200 @@ +# Bubbles + +

+ The Bubbles Logo +

+ +[![Latest Release](https://img.shields.io/github/release/charmbracelet/bubbles.svg)](https://github.com/charmbracelet/bubbles/releases) +[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://pkg.go.dev/github.com/charmbracelet/bubbles) +[![Build Status](https://github.com/charmbracelet/bubbles/workflows/build/badge.svg)](https://github.com/charmbracelet/bubbles/actions) +[![Go ReportCard](https://goreportcard.com/badge/charmbracelet/bubbles)](https://goreportcard.com/report/charmbracelet/bubbles) + +Some components for [Bubble Tea](https://github.com/charmbracelet/bubbletea) +applications. These components are used in production in [Glow][glow], +[Charm][charm] and [many other applications][otherstuff]. + +[glow]: https://github.com/charmbracelet/glow +[charm]: https://github.com/charmbracelet/charm +[otherstuff]: https://github.com/charmbracelet/bubbletea/#bubble-tea-in-the-wild + +## Spinner + +Spinner Example + +A spinner, useful for indicating that some kind an operation is happening. +There are a couple default ones, but you can also pass your own ”frames.” + +- [Example code, basic spinner](https://github.com/charmbracelet/tea/tree/master/examples/spinner/main.go) +- [Example code, various spinners](https://github.com/charmbracelet/tea/tree/master/examples/spinners/main.go) + +## Text Input + +Text Input Example + +A text input field, akin to an `` in HTML. Supports unicode, +pasting, in-place scrolling when the value exceeds the width of the element and +the common, and many customization options. + +- [Example code, one field](https://github.com/charmbracelet/tea/tree/master/examples/textinput/main.go) +- [Example code, many fields](https://github.com/charmbracelet/tea/tree/master/examples/textinputs/main.go) + +## Progress + +Progressbar Example + +A simple, customizable progress meter, with optional animation via +[Harmonica][harmonica]. Supports solid and gradient fills. The empty and filled +runes can be set to whatever you'd like. The percentage readout is customizable +and can also be omitted entirely. + +- [Animated example](https://github.com/charmbracelet/bubbletea/blob/master/examples/progress-animated/main.go) +- [Static example](https://github.com/charmbracelet/bubbletea/blob/master/examples/progress-static/main.go) + +[harmonica]: https://github.com/charmbracelet/harmonica + +## Paginator + +Paginator Example + +A component for handling pagination logic and optionally drawing pagination UI. +Supports "dot-style" pagination (similar to what you might see on iOS) and +numeric page numbering, but you could also just use this component for the +logic and visualize pagination however you like. + +- [Example code](https://github.com/charmbracelet/bubbletea/blob/master/examples/pager/main.go) + +## Viewport + +Viewport Example + +A viewport for vertically scrolling content. Optionally includes standard +pager keybindings and mouse wheel support. A high performance mode is available +for applications which make use of the alternate screen buffer. + +- [Example code](https://github.com/charmbracelet/tea/tree/master/examples/pager/main.go) + +This component is well complemented with [Reflow][reflow] for ANSI-aware +indenting and text wrapping. + +[reflow]: https://github.com/muesli/reflow + +## List + +List Example + +A customizable, batteries-included component for browsing a set of items. +Features pagination, fuzzy filtering, auto-generated help, an activity spinner, +and status messages, all of which can be enabled and disabled as needed. +Extrapolated from [Glow][glow]. + +- [Example code, default list](https://github.com/charmbracelet/tea/tree/master/examples/list-default/main.go) +- [Example code, simple list](https://github.com/charmbracelet/tea/tree/master/examples/list-simple/main.go) +- [Example code, all features](https://github.com/charmbracelet/tea/tree/master/examples/list-fancy/main.go) + +## Timer + +A simple, flexible component for counting down. The update frequency and output +can be customized as you like. + +Timer example + +- [Example code](https://github.com/charmbracelet/bubbletea/blob/master/examples/timer/main.go) + +## Stopwatch + +Stopwatch example + +A simple, flexible component for counting up. The update frequency and output +can be customized as you see fit. + +- [Example code](https://github.com/charmbracelet/bubbletea/blob/master/examples/stopwatch/main.go) + +## Help + +Help Example + +A customizable horizontal mini help view that automatically generates itself +from your keybindings. It features single and multi-line modes, which the user +can optionally toggle between. It will truncate gracefully if the terminal is +too wide for the content. + +- [Example code](https://github.com/charmbracelet/bubbletea/blob/master/examples/help/main.go) + +## Key + +A non-visual component for managing keybindings. It’s useful for allowing users +to remap keybindings as well as generating help views corresponding to your +keybindings. + +```go +type KeyMap struct { + Up key.Binding + Down key.Binding +} + +var DefaultKeyMap = KeyMap{ + Up: key.NewBinding( + key.WithKeys("k", "up"), // actual keybindings + key.WithHelp("↑/k", "move up"), // corresponding help text + ), + Down: key.NewBinding( + key.WithKeys("j", "down"), + key.WithHelp("↓/j", "move down"), + ), +} + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, DefaultKeyMap.Up): + // The user pressed up + case key.Matches(msg, DefaultKeyMap.Down): + // The user pressed down + } + } + return m, nil +} +``` + +## Additional Bubbles + + + +- [76creates/stickers](https://github.com/76creates/stickers): Responsive + flexbox and table components. +- [calyptia/go-bubble-table](https://github.com/calyptia/go-bubble-table): An + interactive, customizable, scrollable table component. +- [erikgeiser/promptkit](https://github.com/erikgeiser/promptkit): A collection + of common prompts for cases like selection, text input, and confirmation. + Each prompt comes with sensible defaults, remappable keybindings, any many + customization options. +- [evertras/bubble-table](https://github.com/Evertras/bubble-table): Interactive, + customizable, paginated tables. +- [knipferrc/teacup](https://github.com/knipferrc/teacup): Various handy + bubbles and utilities for building Bubble Tea applications. +- [mritd/bubbles](https://github.com/mritd/bubbles): Some general-purpose + bubbles. Inputs with validation, menu selection, a modified progressbar, and + so on. +- [treilik/bubbleboxer](https://github.com/treilik/bubbleboxer): Layout + multiple bubbles side-by-side in a layout-tree. +- [treilik/bubblelister](https://github.com/treilik/bubblelister): An alternate + list that is scrollable without pagination and has the ability to contain + other bubbles as list items. + +If you’ve built a Bubble you think should be listed here, +[let us know](mailto:vt100@charm.sh). + +## License + +[MIT](https://github.com/charmbracelet/teaparty/raw/master/LICENSE) + +--- + +Part of [Charm](https://charm.sh). + +The Charm logo + +Charm 热爱开源 • Charm loves open source + +[charm]: https://charm.sh/ diff --git a/cells/std/cli/dummy_data/random-readme-2.md b/cells/std/cli/dummy_data/random-readme-2.md new file mode 100644 index 00000000..729e1983 --- /dev/null +++ b/cells/std/cli/dummy_data/random-readme-2.md @@ -0,0 +1,199 @@ + + +
+ +

Standard

+

Ship today with architecture for tomorrow +

+ + + +[Standard][std] is THE opinionated, generic, +[Nix][nix] [Flakes][nix_flakes] framework +that will allow you to grow and cultivate +Nix Cells with ease. + +Nix Cells are the fine art of code organization +using flakes. + +As a Nix Cell Cultivator, you can focus on building +for your use cases and ride on the marvels of nix flakes +while wasting virtually no thoughts on boilerplate +code organization. + +Because [Standard][std] is a proper framework, +you benefit from continued performance +and feature upgrades over time with minimum effort. :sparkles: + +**Code Organization** + +[Standard][std] has a pre-defined place +for all your code. +Packages, applications, functions, libraries, modules, profiles: +they all have a home. + +**Developer Experience** + +[Standard][std] projects +are as declarative as possible; +we eliminate most boilerplate; +zero-config workflows... +everything Just Works™. + +**DevOps Professionals,`nix`-loving** + +[Standard][std] doesn't just throw more options at you. +It gives you and your team something much more valuable: _guidance_. + +## How it's organized + +[Standard][std] places all the code in a directory of your choice. + +![](./artwork/model.png) + +Related code is grouped into **Cells** +that can be composed together +to form any functionality you can imagine. + +A Cell provides functionality through **Organelles** of **Clade**: + +- Runnables +- Installables +- Functions +- Data + +The built-in default **Organelles** are: + +- Applications (Runnables) + + Instructions that can be run. + For example: `cd`, `ls`, and `cat` are applications. + +- Packages (Installables) + + Contents (files and/or directories) + generated in a pure and reproducible way, + also known as [derivations][nix_drv]. + +- Libraries (Functions) + + Instructions on how to turn the given inputs + into something else. + + They act like a library + that you and others can use + in order to abstract, share + and re-use code. + +A potential alternative to the default **Organelle** _types_ could be: + +- NixOS Modules (Functions) +- NixOS Profiles (Functions) +- DevShell Profiles (Functions) +- Just Tasks (Runnables) +- Entrypoints (Runnables) + +### Hello World application + +[Standard][std] features a special project structure +that brings some awesome innovation +to this often overlooked (but important) part of your project. +With the default **Organelles**, an `app.nix` file tells [Standard][std] +that we are creating an Application. +`flake.nix` is in charge +of explicitly defining +the inputs of your project. + +- `/my/project` + + - `/flake.nix` + + ```nix + { + inputs.std.url = "github:divnix/std"; + + outputs = { std, ... } @ inputs: + std.grow { + inherit inputs; + cellsFrom = ./cells; + }; + } + ``` + + - `/cells` + + - `/hello` + + - `/apps.nix` + + ```nix + { inputs + , cell + }: + { + default = inputs.nixpkgs.stdenv.mkDerivation rec { + pname = "hello"; + version = "2.10"; + src = inputs.nixpkgs.fetchurl { + url = "mirror://gnu/hello/${pname}-${version}.tar.gz"; + sha256 = "0ssi1wpaf7plaswqqjwigppsg5fyh99vdlb9kzl7c9lng89ndq1i"; + }; + }; + } + ``` + +```bash +$ nix run /my/project#hello +Hello, world! +``` + +You see? from nothing +to running your first application +in just a few seconds :sparkles: + +## Examples + +If you'd like to see some examples +of what a [Standard][std] project looks like, +take a look at the following: + +- [Bitte Cells][bitte-cells] + +:construction: Work in progress, would like to help us extend this section? + +## Contributions + +Please get ourself the appropriate environment: + +### With `direnv` + +```console +direnv allow +``` + +### Without `direnv` + +```console +nix develop ./devshell -c $SHELL +menu +``` + +--- + +[bitte-cells]: https://github.com/input-output-hk/bitte-cells +[cross_compiler]: https://en.wikipedia.org/wiki/Cross_compiler +[hydra]: https://github.com/NixOS/hydra +[nix_drv]: https://nixos.org/manual/nix/unstable/expressions/derivations.html +[nix_flakes]: https://nixos.wiki/wiki/Flakes +[nix]: https://nixos.org/manual/nix/unstable +[std]: https://github.com/divnix/std diff --git a/cells/std/cli/dummy_data/randomitems.go b/cells/std/cli/dummy_data/randomitems.go new file mode 100644 index 00000000..26e2aa54 --- /dev/null +++ b/cells/std/cli/dummy_data/randomitems.go @@ -0,0 +1,290 @@ +package dummy_data + +import ( + "math/rand" + "sync" + + "github.com/divnix/std/cells/std/cli/data" +) + +type RandomItemGenerator struct { + names []string + nameIndex int + organelles []string + organelleIndex int + cells []string + cellIndex int + clades []string + cladeIndex int + descs []string + descIndex int + readmes []string + readmeIndex int + mtx *sync.Mutex + shuffle *sync.Once +} + +func (r *RandomItemGenerator) reset() { + r.mtx = &sync.Mutex{} + r.shuffle = &sync.Once{} + + // stdMetaJson := `[{"__std_name": "name","__std_organelle": "organelle","__std_cell": "cell","__std_clade": "clade","__std_description": "A description ..."}]` + r.names = []string{ + "name", + "default", + "backend", + } + + r.cells = []string{ + "cloud", + "metal", + "automation", + } + + r.organelles = []string{ + "oci-images", + "nomadEnvs", + "hydrationProfile", + "bitteProfile", + "constants", + "entrypoints", + "packages", + "healthChecks", + } + + r.clades = []string{ + "data", + "functions", + "installables", + "runnables", + } + + r.descs = []string{ + "A little weird", + "Bold flavor", + "Can’t get enough", + "Delectable", + "Expensive", + "Expired", + "Exquisite", + "Fresh", + "Gimme", + "In season", + "Kind of spicy", + "Looks fresh", + "Looks good to me", + "Maybe not", + "My favorite", + "Oh my", + "On sale", + "Organic", + "Questionable", + "Really fresh", + "Refreshing", + "Salty", + "Scrumptious", + "Delectable", + "Slightly sweet", + "Smells great", + "Tasty", + "Too ripe", + "At last", + "What?", + "Wow", + "Yum", + "Maybe", + "Sure, why not?", + } + + r.readmes = []string{ + "./dummy_data/random-readme-1.md", + "./dummy_data/random-readme-2.md", + "", + } + + r.shuffle.Do(func() { + shuf := func(x []string) { + rand.Shuffle(len(x), func(i, j int) { x[i], x[j] = x[j], x[i] }) + } + shuf(r.names) + shuf(r.cells) + shuf(r.organelles) + shuf(r.clades) + shuf(r.descs) + shuf(r.readmes) + }) +} + +func (r *RandomItemGenerator) Next() data.Item { + if r.mtx == nil { + r.reset() + } + + r.mtx.Lock() + defer r.mtx.Unlock() + + var ( + actionsGenerator randomActionGenerator + ) + // Make actions + const numItems = 3 + items := make([]data.Action, numItems) + for i := 0; i < numItems; i++ { + items[i] = actionsGenerator.next() + } + + i := data.Item{ + StdName: r.names[r.nameIndex], + StdOrganelle: r.organelles[r.organelleIndex], + StdCell: r.cells[r.cellIndex], + StdClade: r.clades[r.cladeIndex], + StdDescription: r.descs[r.descIndex], + StdReadme: r.readmes[r.readmeIndex], + StdCladeActions: items, + } + + r.nameIndex++ + if r.nameIndex >= len(r.names) { + r.nameIndex = 0 + } + + r.organelleIndex++ + if r.organelleIndex >= len(r.organelles) { + r.organelleIndex = 0 + } + + r.cellIndex++ + if r.cellIndex >= len(r.cells) { + r.cellIndex = 0 + } + + r.cladeIndex++ + if r.cladeIndex >= len(r.clades) { + r.cladeIndex = 0 + } + + r.descIndex++ + if r.descIndex >= len(r.descs) { + r.descIndex = 0 + } + + r.readmeIndex++ + if r.readmeIndex >= len(r.readmes) { + r.readmeIndex = 0 + } + + return i +} + +type randomActionGenerator struct { + actionNames []string + actionNameIndex int + actionCommands [][]string + actionCommandIndex int + actionDescs []string + actionDescIndex int + mtx *sync.Mutex + shuffle *sync.Once +} + +func (r *randomActionGenerator) reset() { + r.mtx = &sync.Mutex{} + r.shuffle = &sync.Once{} + r.actionNames = []string{ + "build", + "run", + "deploy", + "serve", + "validate", + "test", + } + r.actionCommands = [][]string{ + {"nix", "run", ".#f.r.a.g.m.e.n.t"}, + {"nix", "build", ".#fragment", "&&", "nomad", "result/job"}, + {"cat", "./cow"}, + {"cowsay", "hi"}, + {"fastlane", "run", "..."}, + {"go", "build", "."}, + } + r.actionDescs = []string{ + "A little weird", + "Bold flavor", + "Can’t get enough", + "Delectable", + "Expensive", + "Expired", + "Exquisite", + "Fresh", + "Gimme", + "In season", + "Kind of spicy", + "Looks fresh", + "Looks good to me", + "Maybe not", + "My favorite", + "Oh my", + "On sale", + "Organic", + "Questionable", + "Really fresh", + "Refreshing", + "Salty", + "Scrumptious", + "Delectable", + "Slightly sweet", + "Smells great", + "Tasty", + "Too ripe", + "At last", + "What?", + "Wow", + "Yum", + "Maybe", + "Sure, why not?", + } + + r.shuffle.Do(func() { + shufStrings := func(x []string) { + rand.Shuffle(len(x), func(i, j int) { x[i], x[j] = x[j], x[i] }) + } + shufLists := func(x [][]string) { + rand.Shuffle(len(x), func(i, j int) { x[i], x[j] = x[j], x[i] }) + } + shufStrings(r.actionNames) + shufLists(r.actionCommands) + shufStrings(r.actionDescs) + }) + +} + +func (r *randomActionGenerator) next() data.Action { + if r.mtx == nil { + r.reset() + } + + r.mtx.Lock() + defer r.mtx.Unlock() + + a := data.Action{ + ActionName: r.actionNames[r.actionNameIndex], + ActionCommand: r.actionCommands[r.actionCommandIndex], + ActionDescription: r.actionDescs[r.actionDescIndex], + } + + r.actionNameIndex++ + if r.actionNameIndex >= len(r.actionNames) { + r.actionNameIndex = 0 + } + + r.actionCommandIndex++ + if r.actionCommandIndex >= len(r.actionCommands) { + r.actionCommandIndex = 0 + } + + r.actionDescIndex++ + if r.actionDescIndex >= len(r.actionDescs) { + r.actionDescIndex = 0 + } + + return a +} diff --git a/cells/std/cli/flake.go b/cells/std/cli/flake.go new file mode 100644 index 00000000..1dca0ad8 --- /dev/null +++ b/cells/std/cli/flake.go @@ -0,0 +1,134 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + + "github.com/TylerBrock/colorjson" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + + "github.com/divnix/std/cells/std/cli/data" + "github.com/divnix/std/cells/std/cli/dummy_data" +) + +type outt struct { + drvPath string `json:"drvPath"` + outputs map[string]string `json:"outputs"` +} + +var ( + currentSystemArgs = []string{"eval", "--raw", "--impure", "--expr", "builtins.currentSystem"} + flakeStdMetaFragment = "%s#__std.%s" + // flakeStdMetaArgs = []string{"eval", "--json", "--option", "warn-dirty", "false"} + flakeStdMetaArgs = []string{"build", "--no-link", "--json", "--option", "warn-dirty", "false"} + flakeStdBuildOut = []map[string]interface{}{} +) + +func fakeData() []data.Item { + var targetsGenerator dummy_data.RandomItemGenerator + const numItems = 24 + items := make([]data.Item, numItems) + for i := 0; i < numItems; i++ { + items[i] = targetsGenerator.Next() + } + return items +} + +func loadFlake() tea.Msg { + var items []data.Item + nix, err := exec.LookPath("nix") + if err != nil { + log.Fatal("You need to install 'nix' in order to use 'std'") + } + + // detect the current system + currentSystem, err := exec.Command(nix, currentSystemArgs...).Output() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + log.Fatalln(exitErr, string(exitErr.Stderr)) + } + log.Fatal(err) + } + + flakeStdMetaFragment = fmt.Sprintf(flakeStdMetaFragment, ".", currentSystem) + flakeStdMetaArgs = append(flakeStdMetaArgs, flakeStdMetaFragment) + + // load the std metadata from the flake + cmd := exec.Command(nix, flakeStdMetaArgs...) + out, err := cmd.Output() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + log.Fatalln(exitErr, string(exitErr.Stderr)) + } + log.Fatal(err) + } + + if err := json.Unmarshal(out, &flakeStdBuildOut); err != nil { + log.Fatal(err) + } + + flakeStdMetaJson, err := os.Open(flakeStdBuildOut[0]["outputs"].(map[string]interface{})["out"].(string)) + if err != nil { + log.Fatal(err) + } + // if we os.Open returns an error then handle it + defer flakeStdMetaJson.Close() + + // read our opened jsonFile as a byte array. + flakeStdMeta, _ := ioutil.ReadAll(flakeStdMetaJson) + if err != nil { + switch exitErr := err.(type) { + case *exec.ExitError: + return exitErrMsg{ + cmd: cmd.String(), + err: exitErr, + } + default: + log.Fatal(err) + } + } + + if err := json.Unmarshal(flakeStdMeta, &items); err != nil { + var obj interface{} + json.Unmarshal(flakeStdMeta, &obj) + f := colorjson.NewFormatter() + f.Indent = 2 + s, _ := f.Marshal(obj) + log.Fatalf("%s - object: %s", err, s) + } + + // var obj interface{} + // json.Unmarshal(flakeStdMeta, &obj) + // f := colorjson.NewFormatter() + // f.Indent = 2 + // s, _ := f.Marshal(obj) + // log.Fatalf("object: %s", s) + + return flakeLoadedMsg{ + // Items: fakeData(), + Items: items, + } +} + +type flakeLoadedMsg struct { + Items []data.Item +} + +type exitErrMsg struct { + cmd string + err *exec.ExitError +} + +func (e exitErrMsg) Error() string { + return fmt.Sprintf( + "%s\nresulted in %s\n\nTraceback:\n\n%s", + lipgloss.NewStyle().Faint(true).Bold(true).Render(e.cmd), + e.err.Error(), + string(e.err.Stderr), + ) +} diff --git a/cells/std/cli/go.mod b/cells/std/cli/go.mod new file mode 100644 index 00000000..898bd4a2 --- /dev/null +++ b/cells/std/cli/go.mod @@ -0,0 +1,12 @@ +module github.com/divnix/std/cells/std/cli + +go 1.16 + +require ( + github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 + github.com/charmbracelet/bubbles v0.10.3 + github.com/charmbracelet/bubbletea v0.20.1-0.20220412151435-14e58aa1f92f + github.com/charmbracelet/lipgloss v0.5.0 + github.com/fatih/color v1.13.0 // indirect + github.com/knipferrc/teacup v0.0.16 +) diff --git a/cells/std/cli/go.sum b/cells/std/cli/go.sum new file mode 100644 index 00000000..2809289a --- /dev/null +++ b/cells/std/cli/go.sum @@ -0,0 +1,114 @@ +github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4= +github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= +github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= +github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/charmbracelet/bubbles v0.10.3 h1:fKarbRaObLn/DCsZO4Y3vKCwRUzynQD9L+gGev1E/ho= +github.com/charmbracelet/bubbles v0.10.3/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA= +github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA= +github.com/charmbracelet/bubbletea v0.20.0 h1:/b8LEPgCbNr7WWZ2LuE/BV1/r4t5PyYJtDb+J3vpwxc= +github.com/charmbracelet/bubbletea v0.20.0/go.mod h1:zpkze1Rioo4rJELjRyGlm9T2YNou1Fm4LIJQSa5QMEM= +github.com/charmbracelet/bubbletea v0.20.1-0.20220412151435-14e58aa1f92f h1:/6cBUPF3UlMueuKtQay4IepufjiMfwHwLyVfU9UDFWo= +github.com/charmbracelet/bubbletea v0.20.1-0.20220412151435-14e58aa1f92f/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4= +github.com/charmbracelet/glamour v0.5.0 h1:wu15ykPdB7X6chxugG/NNfDUbyyrCLV9XBalj5wdu3g= +github.com/charmbracelet/glamour v0.5.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc= +github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM= +github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8= +github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= +github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= +github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/knipferrc/teacup v0.0.16 h1:Ie3DYKo4uOyurZ6xzS/gbNFoKbOdETsKcRYsyvyjTvc= +github.com/knipferrc/teacup v0.0.16/go.mod h1:4BzkrwvGGn31qnbRk+4MXB34UZvksu1K/QjKWHhfgm0= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM= +github.com/microcosm-cc/bluemonday v1.0.18 h1:6HcxvXDAi3ARt3slx6nTesbvorIc3QeTzBNRvWktHBo= +github.com/microcosm-cc/bluemonday v1.0.18/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA= +github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.0 h1:SOpr+CfyVNce341kKqvbhhzQhBPyJRXQaCtn03Pae1Q= +github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= +github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= +github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI= +github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= +github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= +github.com/yuin/goldmark v1.4.11 h1:i45YIzqLnUc2tGaTlJCyUxSG8TvgyGqhqOZOUKIjJ6w= +github.com/yuin/goldmark v1.4.11/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= +github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os= +github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20220321031419-a8550c1d254a/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c= +golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw= +golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0= +golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cells/std/cli/keys/keys.go b/cells/std/cli/keys/keys.go new file mode 100644 index 00000000..32ff5f7c --- /dev/null +++ b/cells/std/cli/keys/keys.go @@ -0,0 +1,135 @@ +package keys + +import ( + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/bubbles/viewport" +) + +const spacebar = " " + +var ( + cursorUp = key.NewBinding(key.WithKeys("k", "up"), key.WithHelp("k/↑", "up")) + cursorDown = key.NewBinding(key.WithKeys("j", "down"), key.WithHelp("j/↓", "down")) + cursorLeft = key.NewBinding(key.WithKeys("left"), key.WithHelp("←", "½ back")) + cursorRight = key.NewBinding(key.WithKeys("right"), key.WithHelp("→", "½ forward")) + pageUp = key.NewBinding(key.WithKeys("pgup"), key.WithHelp("pgup", "1 back")) + pageDown = key.NewBinding(key.WithKeys("pgdown", spacebar), key.WithHelp("pgdn", "1 forward")) + home = key.NewBinding(key.WithKeys("home"), key.WithHelp("home", "go to start")) + end = key.NewBinding(key.WithKeys("end"), key.WithHelp("end", "go to end")) + enter = key.NewBinding(key.WithKeys("enter"), key.WithHelp("⏎", "execute")) + search = key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "filter")) + showReadme = key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "inspect")) + closeReadme = key.NewBinding(key.WithKeys("?", "esc"), key.WithHelp("?", "close")) + quit = key.NewBinding(key.WithKeys("q"), key.WithHelp("q", "quit")) + forceQuit = key.NewBinding(key.WithKeys("ctrl+c")) + toggleFocus = key.NewBinding(key.WithKeys("tab", "shift+tab"), key.WithHelp("⇥", "toggle focus")) + cycleTab = key.NewBinding(key.WithKeys("tab"), key.WithHelp("⇥", "cycle tabs")) + reverseCycleTab = key.NewBinding(key.WithKeys("shift+tab")) +) + +type AppKeyMap struct { + ToggleFocus key.Binding + FocusLeft key.Binding + FocusRight key.Binding + ShowReadme key.Binding + Quit key.Binding + ForceQuit key.Binding +} + +func NewAppKeyMap() *AppKeyMap { + return &AppKeyMap{ + ToggleFocus: toggleFocus, + FocusLeft: cursorLeft, + FocusRight: cursorRight, + ShowReadme: showReadme, + ForceQuit: forceQuit, + Quit: quit, + } +} + +type ReadmeKeyMap struct { + viewport.KeyMap + CloseReadme key.Binding + CycleTab key.Binding + ReverseCycleTab key.Binding +} + +func NewReadmeKeyMap() *ReadmeKeyMap { + m := &ReadmeKeyMap{ + CloseReadme: closeReadme, + CycleTab: cycleTab, + ReverseCycleTab: reverseCycleTab, + } + m.PageDown = pageUp + m.PageUp = pageDown + m.HalfPageUp = cursorLeft + m.HalfPageDown = cursorRight + m.Up = cursorUp + m.Down = cursorDown + return m +} + +// DefaultListKeyMap returns a default set of keybindings. +func DefaultListKeyMap() list.KeyMap { + return list.KeyMap{ + // Browsing. + CursorUp: cursorUp, + CursorDown: cursorDown, + PrevPage: pageUp, + NextPage: pageDown, + GoToStart: home, + GoToEnd: end, + Filter: search, + + // Filtering. + ClearFilter: key.NewBinding( + key.WithKeys("esc"), + key.WithHelp("esc", "clear filter"), + ), + CancelWhileFiltering: key.NewBinding( + key.WithKeys("esc"), + key.WithHelp("esc", "cancel"), + ), + AcceptWhileFiltering: key.NewBinding( + key.WithKeys("enter", "tab", "up", "down"), + key.WithHelp("enter", "apply filter"), + ), + } +} + +type ActionDelegateKeyMap struct { + Exec key.Binding + Inspect key.Binding + QuitInspect key.Binding +} + +// Additional short help entries. This satisfies the help.KeyMap interface and +// is entirely optional. +func (d ActionDelegateKeyMap) ShortHelp() []key.Binding { + return []key.Binding{ + d.Exec, + d.Inspect, + d.QuitInspect, + } +} + +func NewActionDelegateKeyMap() *ActionDelegateKeyMap { + return &ActionDelegateKeyMap{ + Exec: enter, + Inspect: showReadme, + QuitInspect: closeReadme, + } +} + +// ViewportKeyMap returns a set of pager-like default keybindings. +func ViewportKeyMap() viewport.KeyMap { + return viewport.KeyMap{ + PageDown: pageUp, + PageUp: pageDown, + HalfPageUp: cursorLeft, + HalfPageDown: cursorRight, + Up: cursorUp, + Down: cursorDown, + } +} diff --git a/cells/std/cli/main.go b/cells/std/cli/main.go new file mode 100644 index 00000000..7e06fcf4 --- /dev/null +++ b/cells/std/cli/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" + + "math/rand" + "os" + "time" + + tea "github.com/charmbracelet/bubbletea" +) + +func main() { + rand.Seed(time.Now().UTC().UnixNano()) + + if err := tea.NewProgram(InitialPage()).Start(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} diff --git a/cells/std/cli/main.scm b/cells/std/cli/main.scm deleted file mode 100644 index 44b88dd2..00000000 --- a/cells/std/cli/main.scm +++ /dev/null @@ -1,336 +0,0 @@ -; SPDX-FileCopyrightText: 2022 The Standard Authors -; SPDX-FileCopyrightText: 2022 The TVL Authors -; SPDX-FileCopyrightText: 2022 Vincent Ambo -; -; SPDX-License-Identifier: MIT - -;; std helps you build & run thing from wherever you are -;; -;; this is a tiny tool designed to ease workflows in ropositories that are -;; modeled after Standard. -;; -;; it enables nix builds & runs from any repository location -;; specifying relative or absolute paths - -(import (chicken base) - (chicken format) - (chicken irregex) - (chicken port) - (chicken process) - (chicken process-context) - (chicken pathname) - (chicken string) - (chicken pretty-print) - (srfi-13) - (fmt) - (matchable) - (medea) - (shell) - (only (chicken io) read-string)) - -(define usage #< - -target: - A target is an absolute or relative path to an organelle of types - 'runnables' & 'installables', that optionally specifies an - attribute if the organelle is not a singleton output. - - run only available on organelles of type 'runnables' - - For example: - - //std/cli - absolute target to a singleton organelle - //foo/bar:baz - absolute target to a multi-output organelle - cli - relative target to a singleton organelle - bar:baz - relative target to a multi-output organelle - -commands: - ls - show list of all outputs - run - run a target (only for 'runnables') - build - build a target - shell - enter a shell with the target's build dependencies - show - peek at the nix derivation -USAGE -) - -;; parse target definitions. trailing slashes on physical targets are -;; allowed for shell autocompletion. -;; -;; component ::= any string without "/" or ":" -;; -;; physical-target ::= -;; | "/" -;; | "/" -;; -;; virtual-target ::= ":" -;; -;; relative-target ::= -;; | -;; | -;; -;; root-anchor ::= "//" -;; -;; target ::= | - -;; read a path component until it looks like something else is coming -(define (read-component first port) - (let ((keep-reading? - (lambda () (not (or (eq? #\/ (peek-char port)) - (eq? #\: (peek-char port)) - (eof-object? (peek-char port))))))) - (let reader ((acc (list first)) - (condition (keep-reading?))) - (if condition (reader (cons (read-char port) acc) (keep-reading?)) - (list->string (reverse acc)))))) - -;; read something that started with a slash. what will it be? -(define (read-slash port) - (if (eq? #\/ (peek-char port)) - (begin (read-char port) - 'root-anchor) - 'path-separator)) - -;; read any target token and leave port sitting at the next one -(define (read-token port) - (match (read-char port) - [#\/ (read-slash port)] - [#\: 'virtual-separator] - [other (read-component other port)])) - -;; read a target into a list of target tokens -(define (read-target target-str) - (call-with-input-string - target-str - (lambda (port) - (let reader ((acc '())) - (if (eof-object? (peek-char port)) - (reverse acc) - (reader (cons (read-token port) acc))))))) - -(define-record target absolute components virtual) -(define (empty-target) (make-target #f '() #f)) - -(define-record-printer (target t out) - (fprintf out (conc (if (target-absolute t) "//" "") - (string-intersperse (target-components t) "/") - (if (target-virtual t) ":" "") - (or (target-virtual t) "")))) - -;; parse and validate a list of target tokens -(define parse-tokens - (lambda (tokens #!optional (mode 'root) (acc (empty-target))) - (match (cons mode tokens) - ;; absolute target - [('root . ('root-anchor . rest)) - (begin (target-absolute-set! acc #t) - (parse-tokens rest 'root acc))] - - ;; relative target minus potential garbage - [('root . (not ('path-separator . _))) - (parse-tokens tokens 'normal acc)] - - ;; virtual target - [('normal . ('virtual-separator . rest)) - (parse-tokens rest 'virtual acc)] - - [('virtual . ((? string? v))) - (begin - (target-virtual-set! acc v) - acc)] - - ;; chomp through all components and separators - [('normal . ('path-separator . rest)) (parse-tokens rest 'normal acc)] - [('normal . ((? string? component) . rest)) - (begin (target-components-set! - acc (append (target-components acc) (list component))) - (parse-tokens rest 'normal acc))] - - ;; nothing more to parse and not in a weird state, all done, yay! - [('normal . ()) acc] - - ;; oh no, we ran out of input too early :( - [(_ . ()) `(error . ,(format "unexpected end of input while parsing ~s target" mode))] - - ;; something else was invalid :( - [_ `(error . ,(format "unexpected ~s while parsing ~s target" (car tokens) mode))]))) - -(define (parse-target target) - (let ((target-str (normalize-pathname target))) ; transforms: // -> / - (parse-tokens (read-target (if (substring=? "/" target-str) - (conc "/" target-str) ; so we put it back in the begining - target-str))))) - -;; turn relative targets into absolute targets based on the current -;; directory -(define (normalise-target t) - (when (not (target-absolute t)) - (target-components-set! t (append (relative-cell-path) - (target-components t))) - (target-absolute-set! t #t)) - t) - -;; nix doesn't care about the distinction between physical and virtual -;; targets, normalise it away -(define (normalised-components t) - (if (target-virtual t) - (append (target-components t) (list (target-virtual t))) - (target-components t))) - -;; return the current repository root as a string -(define std--cell-root #f) -(define (cell-root) - (or std--cell-root - (begin - (set! std--cell-root - (normalize-pathname (get-environment-variable "CELL_ROOT"))) - std--cell-root))) - -;; determine the current path relative to the cell root -;; and return it as a list of path components. -(define (relative-cell-path) - (string-split - (substring (current-directory) (string-length (cell-root))) "/")) - -;; escape a string for interpolation in nix code -(define (nix-escape str) - (string-translate* str '(("\"" . "\\\"") - ("${" . "\\${")))) - - -;; get current system -(define (current-system) - (capture "nix eval --raw --impure --expr builtins.currentSystem")) - -;; create a nix path to build the attribute at the specified target -(define (nix-url-for target) - (let ((parts (normalised-components (normalise-target target))) - (system (current-system))) - (match parts - [(cell organelle attr) (conc ".#" system "." cell "." organelle "." attr)] - [(cell organelle) (conc ".#" system "." cell "." organelle ".default")]))) - -;; exit and complain at the user if something went wrong -(define (std-error message) - (format (current-error-port) "[std] error: ~A~%" message) - (exit 1)) - -(define (guarantee-success value) - (match value - [('error . message) (std-error message)] - [_ value])) - -(define (execute-ls) - (let ((cmd (conc "nix eval --json --option warn-dirty false .#__std." (current-system)))) - (read-json (capture ,cmd)))) - -(define (ls args) - (match args - [() (ls-no-args)] - [other (print "not yet implemented")])) - -(define (ls-no-args) - (let* ((result (execute-ls)) - (lines (car (map ls-level-1 result))) - (formatted-parts (map ls-format-parts lines)) - (pre (string-join (map car formatted-parts) "\n")) - (mid (string-join (map cadr formatted-parts) "\n")) - (end (string-join (map caddr formatted-parts) "\n"))) - (fmt #t (tabular (dsp pre) " " (dsp mid) " - " (dsp end))))) - -(define (ls-format-parts l) - (match l - [(cell organelle clade description) - (list - (sprintf "//~A/~A" cell organelle) - (sprintf "(~A)" clade) - description)] - [(cell organelle name clade description) - (list - (sprintf "//~A/~A:~A" cell organelle name) - (sprintf "(~A)" clade) - description)])) - -(define (ls-level-1 l) (map ls-level-2 (cdr l))) -(define (ls-level-2 l) (car (map ls-level-3 (cdr l)))) -(define (ls-level-3 l) - (let* - ((name (car l)) - (value (cdr l)) - (cell (alist-ref '__std_cell value)) - (clade (alist-ref '__std_clade value)) - (description (alist-ref '__std_description value)) - (organelle (alist-ref '__std_organelle value))) - (if (equal? name 'default) - (list cell organelle clade description) - (list cell organelle name clade description)))) - -(define (execute-run t args) - (let ((url (nix-url-for t))) - (printf "[std] running target ~A~%" t) - (process-execute "nix" (append (list "run" "--option" "warn-dirty" "false" url "--") args)))) - -(define (run args) - (match args - [() (print "not yet implemented")] - - ;; single argument should be a target spec - [(arg args ...) (execute-run - (guarantee-success (parse-target arg)) args)] - - [other (print "not yet implemented")])) - -(define (execute-build t) - (let ((url (nix-url-for t))) - (printf "[std] building target ~A~%" t) - (process-execute "nix" - (list "build" "--option" "warn-dirty" "false" url "--show-trace")))) - -(define (build args) - (match args - [() (print "not yet implemented")] - - ;; single argument should be a target spec - [(arg) (execute-build - (guarantee-success (parse-target arg)))] - - [other (print "not yet implemented")])) - -(define (execute-shell t) - (let ((url (nix-url-for t)) - (user-shell (or (get-environment-variable "SHELL") "bash"))) - (printf "[std] entering devshell for ~A~%" t) - (process-execute "nix" - (list "develop" "--option" "warn-dirty" "false" url "--command" user-shell)))) - -(define (shell args) - (match args - [() (print "not yet implemented")] - [(arg) (execute-shell - (guarantee-success (parse-target arg)))] - [other (print "not yet implemented")])) - -(define (execute-show t) - (let ((url (nix-url-for t))) - (printf "[std] showing nix derivation of target ~A~%" t) - (process-execute "nix" (list "edit" "--option" "warn-dirty" "false" url)))) - -(define (show args) - (match args - [() (print "not yet implemented")] - [(arg) (execute-show - (guarantee-success (parse-target arg)))] - [other (print "not yet implemented")])) - -(define (main args) - (match args - [() (print usage)] - [("ls" . _) (ls (cdr args))] - [("run" . _) (run (cdr args))] - [("build" . _) (build (cdr args))] - [("shell" . _) (shell (cdr args))] - [("show" . _) (show (cdr args))] - [other (begin (print "unknown command: std " args) - (print usage))])) - -(main (command-line-arguments)) diff --git a/cells/std/cli/models/actions.go b/cells/std/cli/models/actions.go new file mode 100644 index 00000000..da467a8f --- /dev/null +++ b/cells/std/cli/models/actions.go @@ -0,0 +1,149 @@ +package models + +import ( + "fmt" + "log" + "os" + "os/exec" + "strings" + "syscall" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + + "github.com/divnix/std/cells/std/cli/data" + "github.com/divnix/std/cells/std/cli/keys" +) + +type ActionInspectMsg string + +func newActionDelegate(keys *keys.ActionDelegateKeyMap) list.DefaultDelegate { + d := list.NewDefaultDelegate() + + d.UpdateFunc = func(msg tea.Msg, m *list.Model) tea.Cmd { + var ( + command []string + args []string + ) + + if i, ok := m.SelectedItem().(data.Action); ok { + command = i.ActionCommand + args = []string{"bash", "-c", strings.Join(command, " ")} + } else { + return nil + } + + execve := func() tea.Msg { + binary, lookErr := exec.LookPath("bash") + if lookErr != nil { + log.Fatal(lookErr) + } + env := os.Environ() + execErr := syscall.Exec(binary, args, env) + if execErr != nil { + log.Fatal(execErr) + } + return nil + } + + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, keys.Exec): + return execve + + case key.Matches(msg, keys.Inspect): + return func() tea.Msg { return ActionInspectMsg(strings.Join(args[2:], " ")) } + } + } + + return nil + } + + help := []key.Binding{keys.Exec} + d.ShortHelpFunc = func() []key.Binding { return help } + d.FullHelpFunc = func() [][]key.Binding { return [][]key.Binding{} } + + return d +} + +type ActionModel struct { + Target *data.Item + List list.Model + Width int + Height int +} + +func (m *ActionModel) SetTarget(t *data.Item) { + m.Target = t + m.List.Title = fmt.Sprintf("Actions for %s", t.StdClade) + m.List.SetItems(t.GetActionItems()) +} + +func (m *ActionModel) Init() tea.Cmd { return nil } + +func (m *ActionModel) Update(msg tea.Msg) (*ActionModel, tea.Cmd) { + var ( + appKeys = keys.NewAppKeyMap() + cmd tea.Cmd + ) + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, appKeys.ToggleFocus), key.Matches(msg, appKeys.FocusLeft), key.Matches(msg, appKeys.FocusRight): + cmd = m.List.ToggleSpinner() + return m, cmd + } + case tea.WindowSizeMsg: + m.List.SetHeight(m.Height) + m.List.SetWidth(m.Width) + return m, nil + } + m.List, cmd = m.List.Update(msg) + return m, cmd +} +func (m *ActionModel) View() string { + return lipgloss.NewStyle().Width(m.Width).Height(m.Height).Render(m.List.View()) +} + +func (m *ActionModel) HelpView() string { + return m.List.Help.View(m) +} + +func (m *ActionModel) ShortHelp() []key.Binding { + // switch off the list's help + m.List.KeyMap.ShowFullHelp.SetEnabled(false) + m.List.KeyMap.CloseFullHelp.SetEnabled(false) + return m.List.ShortHelp() +} + +func (m *ActionModel) FullHelp() [][]key.Binding { + kb := [][]key.Binding{{}} + return kb +} + +func NewAction() *ActionModel { + + actionDelegateKeys := keys.NewActionDelegateKeyMap() + delegate := newActionDelegate(actionDelegateKeys) + actionList := list.New([]list.Item{}, delegate, 0, 0) + actionList.Title = fmt.Sprintf("Actions") + actionList.KeyMap = keys.DefaultListKeyMap() + actionList.SetShowPagination(false) + actionList.SetShowHelp(false) + actionList.SetShowStatusBar(false) + actionList.SetFilteringEnabled(false) + actionList.DisableQuitKeybindings() + + return &ActionModel{List: actionList} +} + +func (m *ActionModel) SelectedItem() *data.Action { + if m.List.SelectedItem() == nil { + return nil + } + var i = m.List.SelectedItem().(data.Action) + return &i +} diff --git a/cells/std/cli/models/readme.go b/cells/std/cli/models/readme.go new file mode 100644 index 00000000..3a6979a2 --- /dev/null +++ b/cells/std/cli/models/readme.go @@ -0,0 +1,303 @@ +package models + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/knipferrc/teacup/markdown" + + "github.com/divnix/std/cells/std/cli/data" + "github.com/divnix/std/cells/std/cli/keys" + "github.com/divnix/std/cells/std/cli/styles" +) + +const ( + noTargetReadme = `Target '%s' has no readme yet. + +To create one, simply drop a file in: + + ${cellsFrom}/%s/%s/%s.md +` + noCellReadme = `Cell '//%s' has no readme yet. + +To create one, simply drop a file in: + + ${cellsFrom}/%s/Readme.md +` + noOrganelleReadme = `Organelle '//%s/%s' has no readme yet. + +To create one, simply drop a file in: + + ${cellsFrom}/%s/%s/Readme.md +` +) + +var ( + + // Tabs. + + activeTabBorder = lipgloss.Border{ + Top: "─", + Bottom: " ", + Left: "│", + Right: "│", + TopLeft: "╭", + TopRight: "╮", + BottomLeft: "┘", + BottomRight: "└", + } + + tabBorder = lipgloss.Border{ + Top: "─", + Bottom: "─", + Left: "│", + Right: "│", + TopLeft: "╭", + TopRight: "╮", + BottomLeft: "┴", + BottomRight: "┴", + } + + tab = lipgloss.NewStyle(). + Border(tabBorder, true). + BorderForeground(styles.Highlight). + Padding(0, 1) + + activeTab = tab.Copy().Border(activeTabBorder, true) + + tabGap = tab.Copy(). + BorderTop(false). + BorderLeft(false). + BorderRight(false) +) + +type ReadmeModel struct { + Target *data.Item + TargetHelp markdown.Bubble + CellHelp markdown.Bubble + OrganelleHelp markdown.Bubble + HasTargetHelp bool + HasCellHelp bool + HasOrganelleHelp bool + Active bool + Width int + Height int + KeyMap *keys.ReadmeKeyMap + Help help.Model + // Focus +} + +type renderCellMarkdownMsg struct { + msg tea.Msg +} +type renderOrganelleMarkdownMsg struct { + msg tea.Msg +} +type renderTargetMarkdownMsg struct { + msg tea.Msg +} + +func (m *ReadmeModel) SetTarget(t *data.Item) { + m.Target = t + m.HasTargetHelp = t.StdReadme != "" + m.HasCellHelp = t.StdCellReadme != "" + m.HasOrganelleHelp = t.StdOrganelleReadme != "" + if m.HasTargetHelp { + m.TargetHelp.Viewport.SetContent(fmt.Sprintf("Rendering %s ...", t.StdReadme)) + } else { + content := lipgloss.NewStyle(). + Width(m.Width). + Height(m.Height). + Render(fmt.Sprintf(noTargetReadme, t.Title(), t.StdCell, t.StdOrganelle, t.StdName)) + m.TargetHelp.Viewport.SetContent(content) + } + if m.HasCellHelp { + m.CellHelp.Viewport.SetContent(fmt.Sprintf("Rendering %s ...", t.StdCellReadme)) + } else { + content := lipgloss.NewStyle(). + Width(m.Width). + Height(m.Height). + Render(fmt.Sprintf(noCellReadme, t.StdCell, t.StdCell)) + m.CellHelp.Viewport.SetContent(content) + } + if m.HasOrganelleHelp { + m.OrganelleHelp.Viewport.SetContent(fmt.Sprintf("Rendering %s ...", t.StdOrganelleReadme)) + } else { + content := lipgloss.NewStyle(). + Width(m.Width). + Height(m.Height). + Render(fmt.Sprintf(noOrganelleReadme, t.StdCell, t.StdOrganelle, t.StdCell, t.StdOrganelle)) + m.OrganelleHelp.Viewport.SetContent(content) + } +} + +func NewReadme() *ReadmeModel { + var ( + th = markdown.New(false, true, lipgloss.AdaptiveColor{}) + ch = markdown.New(false, true, lipgloss.AdaptiveColor{}) + oh = markdown.New(false, true, lipgloss.AdaptiveColor{}) + ) + th.Viewport.KeyMap = keys.ViewportKeyMap() + ch.Viewport.KeyMap = keys.ViewportKeyMap() + oh.Viewport.KeyMap = keys.ViewportKeyMap() + return &ReadmeModel{ + TargetHelp: th, + CellHelp: ch, + OrganelleHelp: oh, + Help: help.New(), + KeyMap: keys.NewReadmeKeyMap(), + } +} +func (m *ReadmeModel) Init() tea.Cmd { + return nil +} + +func (m *ReadmeModel) RenderMarkdown() tea.Cmd { + var ( + cmds []tea.Cmd + cmd tea.Cmd + ) + m.TargetHelp.SetIsActive(true) + if m.HasCellHelp { + cmd = func() tea.Msg { + return renderCellMarkdownMsg{m.CellHelp.SetFileName(m.Target.StdCellReadme)()} + } + cmds = append(cmds, cmd) + } + if m.HasOrganelleHelp { + cmd = func() tea.Msg { + return renderOrganelleMarkdownMsg{m.OrganelleHelp.SetFileName(m.Target.StdOrganelleReadme)()} + } + cmds = append(cmds, cmd) + } + if m.HasTargetHelp { + cmd = func() tea.Msg { + return renderTargetMarkdownMsg{m.TargetHelp.SetFileName(m.Target.StdReadme)()} + } + cmds = append(cmds, cmd) + } + return tea.Batch(cmds...) +} + +func (m *ReadmeModel) Update(msg tea.Msg) (*ReadmeModel, tea.Cmd) { + var ( + cmd tea.Cmd + ) + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + // activate and deactivate help + // ShowHelp shadows CloseHelp in case of the toggle key '?' + case key.Matches(msg, m.KeyMap.CloseReadme): + m.Active = false + m.CellHelp.SetIsActive(false) + m.OrganelleHelp.SetIsActive(false) + m.TargetHelp.SetIsActive(false) + return m, nil + case key.Matches(msg, m.KeyMap.CycleTab): + if m.TargetHelp.Active { + m.TargetHelp.SetIsActive(false) + m.CellHelp.SetIsActive(true) + } else if m.CellHelp.Active { + m.CellHelp.SetIsActive(false) + m.OrganelleHelp.SetIsActive(true) + } else if m.OrganelleHelp.Active { + m.OrganelleHelp.SetIsActive(false) + m.TargetHelp.SetIsActive(true) + } + return m, nil + case key.Matches(msg, m.KeyMap.ReverseCycleTab): + if m.TargetHelp.Active { + m.TargetHelp.SetIsActive(false) + m.OrganelleHelp.SetIsActive(true) + } else if m.CellHelp.Active { + m.CellHelp.SetIsActive(false) + m.TargetHelp.SetIsActive(true) + } else if m.OrganelleHelp.Active { + m.OrganelleHelp.SetIsActive(false) + m.CellHelp.SetIsActive(true) + } + return m, nil + } + case tea.WindowSizeMsg: + m.CellHelp.SetSize(m.Width, m.Height) + m.OrganelleHelp.SetSize(m.Width, m.Height) + m.TargetHelp.SetSize(m.Width, m.Height) + case renderCellMarkdownMsg: + m.CellHelp, cmd = m.CellHelp.Update(msg.msg) + return m, cmd + case renderOrganelleMarkdownMsg: + m.OrganelleHelp, cmd = m.OrganelleHelp.Update(msg.msg) + return m, cmd + case renderTargetMarkdownMsg: + m.TargetHelp, cmd = m.TargetHelp.Update(msg.msg) + return m, cmd + } + if m.TargetHelp.Active { + m.TargetHelp, cmd = m.TargetHelp.Update(msg) + } else if m.CellHelp.Active { + m.CellHelp, cmd = m.CellHelp.Update(msg) + } else if m.OrganelleHelp.Active { + m.OrganelleHelp, cmd = m.OrganelleHelp.Update(msg) + } + return m, cmd +} + +func (m *ReadmeModel) View() string { + // Tabs + var ( + tabs []string + content string + ) + if m.CellHelp.Active { + tabs = append(tabs, activeTab.Render(fmt.Sprintf("Cell: %s", m.Target.StdCell))) + content = m.CellHelp.View() + } else { + tabs = append(tabs, tab.Render(fmt.Sprintf("Cell: %s", m.Target.StdCell))) + } + if m.OrganelleHelp.Active { + tabs = append(tabs, activeTab.Render(fmt.Sprintf("Organelle: %s", m.Target.StdOrganelle))) + content = m.OrganelleHelp.View() + } else { + tabs = append(tabs, tab.Render(fmt.Sprintf("Organelle: %s", m.Target.StdOrganelle))) + } + if m.TargetHelp.Active { + tabs = append(tabs, activeTab.Render(fmt.Sprintf("Target: %s", m.Target.StdName))) + content = m.TargetHelp.View() + } else { + tabs = append(tabs, tab.Render(fmt.Sprintf("Target: %s", m.Target.StdName))) + } + + row := lipgloss.JoinHorizontal(lipgloss.Top, tabs...) + gap := tabGap.Render(strings.Repeat(" ", max(0, m.Width-lipgloss.Width(row)-2))) + row = lipgloss.JoinHorizontal(lipgloss.Bottom, row, gap) + + return lipgloss.JoinVertical(lipgloss.Top, row, content) +} + +func (m *ReadmeModel) ShortHelp() []key.Binding { + kb := []key.Binding{ + m.KeyMap.Up, + m.KeyMap.Down, + m.KeyMap.HalfPageUp, + m.KeyMap.HalfPageDown, + m.KeyMap.CycleTab, + m.KeyMap.CloseReadme, + } + return kb +} + +func (m *ReadmeModel) FullHelp() [][]key.Binding { + kb := [][]key.Binding{{}} + return kb +} +func max(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/cells/std/cli/models/targets.go b/cells/std/cli/models/targets.go new file mode 100644 index 00000000..d280d642 --- /dev/null +++ b/cells/std/cli/models/targets.go @@ -0,0 +1,89 @@ +package models + +import ( + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + + "github.com/divnix/std/cells/std/cli/data" + "github.com/divnix/std/cells/std/cli/keys" +) + +type TargetModel struct { + List list.Model + Width int + Height int +} + +func (m *TargetModel) Init() tea.Cmd { return nil } +func (m *TargetModel) Update(msg tea.Msg) (*TargetModel, tea.Cmd) { + var ( + appKeys = keys.NewAppKeyMap() + cmd tea.Cmd + ) + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, appKeys.ToggleFocus), key.Matches(msg, appKeys.FocusLeft), key.Matches(msg, appKeys.FocusRight): + cmd = m.List.ToggleSpinner() + return m, cmd + } + case tea.WindowSizeMsg: + m.List.SetHeight(m.Height) + m.List.SetWidth(m.Width) + return m, nil + } + m.List, cmd = m.List.Update(msg) + return m, cmd +} +func (m *TargetModel) View() string { + return lipgloss.NewStyle().Width(m.Width).Height(m.Height).Render(m.List.View()) +} + +func InitialTarget() *TargetModel { + + targetList := list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) + targetList.Title = "Target" + targetList.KeyMap = keys.DefaultListKeyMap() + targetList.SetFilteringEnabled(true) + targetList.StartSpinner() + targetList.DisableQuitKeybindings() + targetList.SetShowHelp(false) + + return &TargetModel{List: targetList} +} + +func (m *TargetModel) SelectedItem() *data.Item { + if m.List.SelectedItem() == nil { + return nil + } + var i = m.List.SelectedItem().(data.Item) + return &i +} + +func (m *TargetModel) SetItems(l []data.Item) { + var numItems = len(l) + // Make list of actions + items := make([]list.Item, numItems) + for j := 0; j < numItems; j++ { + items[j] = l[j] + } + m.List.SetItems(items) +} + +func (m *TargetModel) HelpView() string { + return m.List.Help.View(m) +} + +func (m *TargetModel) ShortHelp() []key.Binding { + // switch off the list's help + m.List.KeyMap.ShowFullHelp.SetEnabled(false) + m.List.KeyMap.CloseFullHelp.SetEnabled(false) + return m.List.ShortHelp() +} + +func (m *TargetModel) FullHelp() [][]key.Binding { + kb := [][]key.Binding{{}} + return kb +} diff --git a/cells/std/cli/styles/styles.go b/cells/std/cli/styles/styles.go new file mode 100644 index 00000000..0d59ee24 --- /dev/null +++ b/cells/std/cli/styles/styles.go @@ -0,0 +1,36 @@ +package styles + +import ( + "github.com/charmbracelet/lipgloss" +) + +var ( + Highlight = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"} + AppStyle = lipgloss.NewStyle().Padding(1, 2) + + ErrorStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(Highlight).Padding(0, 1) + + TargetStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(Highlight) + + ActionInspectionStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(Highlight).Padding(0, 1).Faint(true) + + ActionStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(Highlight) + + ReadmeStyle = lipgloss.NewStyle(). + // BorderStyle(lipgloss.NormalBorder()). + BorderForeground(Highlight) + + LegendStyle = lipgloss.NewStyle().Padding(1, 0, 0, 2) + + TitleStyle = lipgloss.NewStyle(). + Foreground(Highlight).Bold(true). + Padding(1, 1) +) diff --git a/cells/std/cli/tui.go b/cells/std/cli/tui.go new file mode 100644 index 00000000..6a95c0ec --- /dev/null +++ b/cells/std/cli/tui.go @@ -0,0 +1,320 @@ +package main + +import ( + "fmt" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/bubbles/spinner" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + + "github.com/divnix/std/cells/std/cli/keys" + "github.com/divnix/std/cells/std/cli/models" + "github.com/divnix/std/cells/std/cli/styles" +) + +type Focus int64 + +const ( + Left Focus = iota + Right +) + +const cmdTemplate = "std %s %s" + +func (s Focus) String() string { + switch s { + case Left: + return "left focus" + case Right: + return "right focus" + } + return "unknown" +} + +type Tui struct { + Target *models.TargetModel + Action *models.ActionModel + Readme *models.ReadmeModel + Keys *keys.AppKeyMap + Legend help.Model + Title string + InspecAction string + Spinner spinner.Model + Loading bool + Error string + Focus + Width int + Height int +} + +func (m *Tui) Init() tea.Cmd { + return tea.Batch( + loadFlake, + tea.EnterAltScreen, + m.Spinner.Tick, + ) +} + +func (m *Tui) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var ( + cmds []tea.Cmd + cmd tea.Cmd + actionKeys = keys.NewActionDelegateKeyMap() + ) + // As soon as targets are loaded, stop the loading spinner + if m.Target.SelectedItem() != nil { + m.Loading = false + } + switch msg := msg.(type) { + case flakeLoadedMsg: + m.Target.SetItems(msg.Items) + return m, nil + + case exitErrMsg: + m.Error = msg.Error() + return m, nil + + case models.ActionInspectMsg: + m.InspecAction = string(msg) + return m, nil + + case spinner.TickMsg: + if m.Loading { + m.Spinner, cmd = m.Spinner.Update(msg) + return m, cmd + } + // effectively disables the list-spinners: + // we're happy with a static vertical line as + // visual focus clue + case tea.KeyMsg: + // quit even during filtering + if key.Matches(msg, m.Keys.ForceQuit) { + return m, tea.Quit + } + // Quit action inspection if enabled. + if m.InspecAction != "" && key.Matches(msg, actionKeys.QuitInspect) { + m.InspecAction = "" + return m, nil + } + // Don't match any of the keys below if we're actively filtering. + if m.Target.List.FilterState() == list.Filtering { + break + } + if key.Matches(msg, m.Keys.Quit) { + return m, tea.Quit + } + // Don't match any of the keys below if no target is selected. + if m.Target.SelectedItem() == nil { + return m, nil + } + switch { + // toggle the help + case key.Matches(msg, m.Keys.ShowReadme): + if m.Focus == Left { + if !m.Readme.Active { + m.Readme.Active = true + cmd = m.Readme.RenderMarkdown() + return m, cmd + } + } + // toggle the focus + case key.Matches(msg, m.Keys.ToggleFocus): + // Don't toggle the focus if we're showing the help. + if m.Readme.Active { + break + } + if m.Focus == Left { + m.Focus = Right + } else { + m.Focus = Left + } + m.Target, cmd = m.Target.Update(msg) + cmds = append(cmds, cmd) + m.Action, cmd = m.Action.Update(msg) + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) + case key.Matches(msg, m.Keys.FocusLeft): + // Don't toggle the focus if we're showing the help. + if m.Readme.Active { + break + } + if m.Focus != Left { + m.Focus = Left + m.Target, cmd = m.Target.Update(msg) + cmds = append(cmds, cmd) + m.Action, cmd = m.Action.Update(msg) + cmds = append(cmds, cmd) + } + return m, tea.Batch(cmds...) + case key.Matches(msg, m.Keys.FocusRight): + // Don't toggle the focus if we're showing the help. + if m.Readme.Active { + break + } + if m.Focus != Right { + m.Focus = Right + m.Target, cmd = m.Target.Update(msg) + cmds = append(cmds, cmd) + m.Action, cmd = m.Action.Update(msg) + cmds = append(cmds, cmd) + } + return m, tea.Batch(cmds...) + } + case tea.WindowSizeMsg: + m.Width = msg.Width + m.Height = msg.Height + // size Target + m.Target.Height = msg.Height - 10 + m.Target.Width = msg.Width*2/3 - 10 + m.Target, _ = m.Target.Update(msg) + // size Action + m.Action.Height = msg.Height - 10 + m.Action.Width = msg.Width*1/3 - 10 + m.Action, _ = m.Action.Update(msg) + // size Readme + m.Readme.Height = msg.Height - 10 + m.Readme.Width = msg.Width - 10 + m.Readme, _ = m.Readme.Update(msg) + return m, nil + } + // route all other messages according to state + if m.Readme.Active { + m.Readme, cmd = m.Readme.Update(msg) + cmds = append(cmds, cmd) + } else if m.Focus == Left { + m.Target, cmd = m.Target.Update(msg) + cmds = append(cmds, cmd) + if m.Target.SelectedItem() != nil { + var target = m.Target.SelectedItem() + m.Readme.SetTarget(target) + m.Action.SetTarget(target) + } else { + m.Action.List.SetItems([]list.Item{}) + } + } else { + m.Action, cmd = m.Action.Update(msg) + cmds = append(cmds, cmd) + } + // As soon as targets are loaded, change the title + if m.Target.SelectedItem() != nil { + if m.Action.SelectedItem() != nil { + m.Title = fmt.Sprintf(cmdTemplate, m.Target.SelectedItem().Title(), m.Action.SelectedItem().Title()) + } else { + m.Title = lipgloss.NewStyle().Faint(true).Render(fmt.Sprintf(cmdTemplate, m.Target.SelectedItem().Title(), "n/a")) + } + } + return m, tea.Batch(cmds...) +} + +func (m *Tui) View() string { + var title string + if m.Loading { + title = styles.TitleStyle.Render("Loading " + m.Spinner.View()) + } else { + title = styles.TitleStyle.Render(m.Title) + } + if m.Readme.Active { + return lipgloss.Place(m.Width, m.Height, lipgloss.Center, lipgloss.Center, + styles.AppStyle.MaxWidth(m.Width).MaxHeight(m.Height).Render( + lipgloss.JoinVertical( + lipgloss.Center, + title, + styles.ReadmeStyle.Render(m.Readme.View()), + styles.LegendStyle.Render(m.Legend.View(m)), + )), + ) + } + + if m.Error != "" { + return lipgloss.Place(m.Width, m.Height, lipgloss.Center, lipgloss.Center, styles. + AppStyle.MaxWidth(m.Width).MaxHeight(m.Height).Render( + lipgloss.JoinVertical( + lipgloss.Center, + title, + styles.ErrorStyle.Width(m.Width-10).Height(m.Height-10).Render(m.Error), + styles.LegendStyle.Render(m.Legend.View(m)), + )), + ) + } + if m.InspecAction != "" { + return lipgloss.Place(m.Width, m.Height, lipgloss.Center, lipgloss.Center, styles. + AppStyle.MaxWidth(m.Width).MaxHeight(m.Height).Render( + lipgloss.JoinVertical( + lipgloss.Center, + title, + lipgloss.JoinHorizontal( + lipgloss.Left, + styles.ActionInspectionStyle.Width(m.Target.Width).Height(m.Target.Height).Render(m.InspecAction), + styles.ActionStyle.Render(m.Action.View()), + ), + styles.LegendStyle.Render(m.Legend.View(m)), + )), + ) + } + + return lipgloss.Place(m.Width, m.Height, lipgloss.Center, lipgloss.Center, styles. + AppStyle.MaxWidth(m.Width).MaxHeight(m.Height).Render( + lipgloss.JoinVertical( + lipgloss.Center, + title, + lipgloss.JoinHorizontal( + lipgloss.Left, + styles.TargetStyle.Width(m.Target.Width).Height(m.Target.Height).Render(m.Target.View()), + styles.ActionStyle.Render(m.Action.View()), + ), + styles.LegendStyle.Render(m.Legend.View(m)), + )), + ) +} + +func (m *Tui) ShortHelp() []key.Binding { + if m.Readme.Active { + return append(m.Readme.ShortHelp(), []key.Binding{ + m.Keys.Quit, + }...) + } + if m.Focus == Left { + if m.Target.List.FilterState() == list.Filtering { + return m.Target.ShortHelp() + } else { + return append(m.Target.ShortHelp(), []key.Binding{ + m.Keys.ToggleFocus, + m.Keys.ShowReadme, + m.Keys.Quit, + }...) + } + } else { + return append(m.Action.ShortHelp(), []key.Binding{ + m.Keys.ToggleFocus, + m.Keys.ShowReadme, + m.Keys.Quit, + }...) + } +} + +func (m *Tui) FullHelp() [][]key.Binding { + kb := [][]key.Binding{{}} + return kb +} + +func InitialPage() *Tui { + + target := models.InitialTarget() + action := models.NewAction() + spin := spinner.New() + spin.Spinner = spinner.Points + + return &Tui{ + Target: target, + Action: action, + Keys: keys.NewAppKeyMap(), + Focus: Left, + Readme: models.NewReadme(), + Legend: help.New(), + Loading: true, + Spinner: spin, + } +} diff --git a/cells/std/data.nix b/cells/std/data.nix new file mode 100644 index 00000000..0c3fbf13 --- /dev/null +++ b/cells/std/data.nix @@ -0,0 +1,6 @@ +{ + inputs, + cell, +}: { + example-data = builtins.fromJSON (builtins.readFile ./data/dummy-data.json); +} diff --git a/cells/std/data/dummy-data.json b/cells/std/data/dummy-data.json new file mode 100644 index 00000000..818c314a --- /dev/null +++ b/cells/std/data/dummy-data.json @@ -0,0 +1,91 @@ +[ + { + "__std_actions": [ + { + "command": [ + "nix", + "run", + "/nix/store/g4ixpmwl9jw99ilf0pfp5ygpisk4zwxr-source#\"x86_64-linux\".\"std\".\"cli\".\"default\"" + ], + "description": "exec this target", + "name": "run" + } + ], + "__std_cell": "std", + "__std_cell_readme": "/nix/store/h227imz56igrh7lgym76hg6af6pfcczz-cells/std/Readme.md", + "__std_clade": "runnables", + "__std_description": "A tui for projects that conform to Standard", + "__std_name": "default", + "__std_organelle": "cli", + "__std_organelle_readme": "/nix/store/h227imz56igrh7lgym76hg6af6pfcczz-cells/std/cli/Readme.md", + "__std_readme": "/nix/store/h227imz56igrh7lgym76hg6af6pfcczz-cells/std/cli/default.md" + }, + { + "__std_actions": [ + { + "command": [ + "nix", + "build", + "--impure", + "--json", + "--no-link", + "--expr", + "let\n pkgs =\n (builtins.getFlake \"${nixpkgs.sourceInfo.outPath}\")\n .legacyPackages\n .${builtins.currentSystem};\n this =\n (builtins.getFlake \"${flake.sourceInfo.outPath}\")\n .\"${fragment}\";\nin\n pkgs.writeTextFile {\n name = \"data-clade-write\";\n text = builtins.toJSON this;\n executable = false;\n destination = \"/data\";\n }\n", + "|", + "jq", + "-r", + "'.[].outputs.out'" + ], + "description": "write to file", + "name": "write" + }, + { + "command": [ + "nix", + "build", + "--impure", + "--expr", + "let\n pkgs =\n (builtins.getFlake \"${nixpkgs.sourceInfo.outPath}\")\n .legacyPackages\n .${builtins.currentSystem};\n this =\n (builtins.getFlake \"${flake.sourceInfo.outPath}\")\n .\"${fragment}\";\nin\n pkgs.writeTextFile {\n name = \"data-clade-write\";\n text = builtins.toJSON this;\n executable = false;\n destination = \"/data\";\n }\n", + "|", + "jq", + "-r", + "'.[].outputs.out'", + "|", + "fx" + ], + "description": "interactively explore (requires: 'fx')", + "name": "explore" + } + ], + "__std_cell": "std", + "__std_cell_readme": "/nix/store/h227imz56igrh7lgym76hg6af6pfcczz-cells/std/Readme.md", + "__std_clade": "data", + "__std_description": "n/a", + "__std_name": "default", + "__std_organelle": "data", + "__std_organelle_readme": "", + "__std_readme": "" + }, + { + "__std_actions": [], + "__std_cell": "std", + "__std_cell_readme": "/nix/storzz-cells/std/Readme.md", + "__std_clade": "functions", + "__std_description": "n/a", + "__std_name": "default", + "__std_organelle": "devshellProfiles", + "__std_organelle_readme": "/nix/store/h227imz56igrh7lgym76hg6af6pfcczz-cells/std/devshellProfiles/Readme.md", + "__std_readme": "/nix/store/h227imz56igrh7lgym76hg6af6pfcczz-cells/std/devshellProfiles/default.md" + }, + { + "__std_actions": [], + "__std_cell": "std", + "__std_cell_readme": "/nix/store/h227imz56igrh7lgym76hg6af6pfcczz-cells/std/Readme.md", + "__std_clade": "functions", + "__std_description": "n/a", + "__std_name": "fromMakesWith", + "__std_organelle": "lib", + "__std_organelle_readme": "/nix/store/h227imz56igrh7lgym76hg6af6pfcczz-cells/std/lib/Readme.md", + "__std_readme": "/nix/store/h227imz56igrh7lgym76hg6af6pfcczz-cells/std/lib/fromMakesWith.md" + } +] diff --git a/cells/std/devshellProfiles/Readme.md b/cells/std/devshellProfiles/Readme.md new file mode 100644 index 00000000..3a4d06f6 --- /dev/null +++ b/cells/std/devshellProfiles/Readme.md @@ -0,0 +1 @@ +# `devshellProfiles` Readme diff --git a/cells/std/devshellProfiles/default.md b/cells/std/devshellProfiles/default.md new file mode 100644 index 00000000..dc87f235 --- /dev/null +++ b/cells/std/devshellProfiles/default.md @@ -0,0 +1 @@ +# `default` Readme diff --git a/cells/std/lib/Readme.md b/cells/std/lib/Readme.md new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/cells/std/lib/Readme.md @@ -0,0 +1 @@ + diff --git a/cells/std/lib/fromMakesWith.md b/cells/std/lib/fromMakesWith.md new file mode 100644 index 00000000..31860c9b --- /dev/null +++ b/cells/std/lib/fromMakesWith.md @@ -0,0 +1 @@ +# `fromMakesWith` Readme diff --git a/clades.nix b/clades.nix new file mode 100644 index 00000000..bdaf0e98 --- /dev/null +++ b/clades.nix @@ -0,0 +1,80 @@ +{nixpkgs}: { + runnables = name: { + inherit name; + clade = "runnables"; + actions = { + system, + flake, + fragment, + }: [ + { + name = "run"; + description = "exec this target"; + command = ["nix" "run" "${flake}#${fragment}"]; + } + ]; + }; + installables = name: { + inherit name; + clade = "installables"; + actions = { + system, + flake, + fragment, + }: [ + { + name = "install"; + description = "install this target"; + command = ["nix" "profile" "install" "${flake}#${fragment}"]; + } + { + name = "upgrade"; + description = "upgrade this target"; + command = ["nix" "profile" "upgrade" "${flake}#${fragment}"]; + } + { + name = "remove"; + description = "remove this target"; + command = ["nix" "profile" "remove" fragment]; + } + ]; + }; + functions = name: { + inherit name; + clade = "functions"; + }; + data = name: { + inherit name; + clade = "data"; + actions = { + system, + flake, + fragment, + }: let + builder = ["nix" "build" "--impure" "--json" "--no-link" "--expr" expr]; + jq = ["|" "${nixpkgs.legacyPackages.${system}.jq}/bin/jq" "-r" "'.[].outputs.out'"]; + fx = ["|" "xargs" "cat" "|" "${nixpkgs.legacyPackages.${system}.fx}/bin/fx"]; + expr = nixpkgs.lib.strings.escapeShellArg '' + let + pkgs = (builtins.getFlake "${nixpkgs.sourceInfo.outPath}").legacyPackages.${system}; + this = (builtins.getFlake "${flake}").${fragment}; + in + pkgs.writeTextFile { + name = "data.json"; + text = builtins.toJSON this; + } + ''; + in [ + { + name = "write"; + description = "write to file"; + command = builder ++ jq; + } + { + name = "explore"; + description = "interactively explore"; + command = builder ++ jq ++ fx; + } + ]; + }; +} diff --git a/devshell/flake.nix b/devshell/flake.nix index e0301a0a..81af39a4 100644 --- a/devshell/flake.nix +++ b/devshell/flake.nix @@ -36,6 +36,27 @@ package = nixpkgs.legacyPackages.reuse; category = "legal"; } + { + package = nixpkgs.legacyPackages.delve; + category = "cli-dev"; + name = "dlv"; + } + { + package = nixpkgs.legacyPackages.go; + category = "cli-dev"; + } + { + package = nixpkgs.legacyPackages.gotools; + category = "cli-dev"; + } + { + package = nixpkgs.legacyPackages.gopls; + category = "cli-dev"; + } + { + package = nixpkgs.legacyPackages.golangci-lint; + category = "cli-dev"; + } ]; imports = [ "${extraModulesPath}/git/hooks.nix" diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..314c3818 --- /dev/null +++ b/flake.lock @@ -0,0 +1,48 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1650194139, + "narHash": "sha256-kurZsqeOw5fpqA/Ig+8tHvbjwzs5P9AE6WUKOX1m6qM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "bd4dffcdb7c577d74745bd1eff6230172bd176d5", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "yants": "yants" + } + }, + "yants": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1645126146, + "narHash": "sha256-XQ1eg4gzXoc7Tl8iXak1uCt3KnsTyxqPtLE+vOoDnrQ=", + "owner": "divnix", + "repo": "yants", + "rev": "77df2be1b3cce9f571c6cf451f786b266a6869cc", + "type": "github" + }, + "original": { + "owner": "divnix", + "repo": "yants", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix index 3f98174e..bbd0b7a1 100644 --- a/flake.nix +++ b/flake.nix @@ -10,31 +10,10 @@ inputs.yants.inputs.nixpkgs.follows = "nixpkgs"; outputs = inputs': let nixpkgs = inputs'.nixpkgs; - validate = import ./validators.nix { - inherit (inputs') yants nixpkgs; - inherit organellePath; - }; + validate = import ./validators.nix {inherit (inputs') yants nixpkgs;}; + paths = import ./paths.nix; + clades = import ./clades.nix {inherit nixpkgs;}; incl = import ./incl.nix {inherit nixpkgs;}; - organellePath = cellsFrom: cellName: organelle: { - file = "${cellsFrom}/${cellName}/${organelle.name}.nix"; - dir = "${cellsFrom}/${cellName}/${organelle.name}/default.nix"; - }; - runnables = name: { - inherit name; - clade = "runnables"; - }; - installables = name: { - inherit name; - clade = "installables"; - }; - functions = name: { - inherit name; - clade = "functions"; - }; - data = name: { - inherit name; - clade = "data"; - }; deSystemize = system: s: if builtins.isAttrs s && builtins.hasAttr "${system}" s then s // s.${system} @@ -61,9 +40,9 @@ inputs, cellsFrom, organelles ? [ - (functions "library") - (runnables "apps") - (installables "packages") + (clades.functions "library") + (clades.runnables "apps") + (clades.installables "packages") ], # if true, export installables _also_ as packages and runnables _also_ as apps as-nix-cli-epiphyte ? true, @@ -100,9 +79,22 @@ Cells = nixpkgs.lib.mapAttrsToList (validate.Cell cellsFrom Organelles) (builtins.readDir cellsFrom); # Set of all std-injected outputs in the project flake in the outpts and inputs.cells format accumulate = builtins.foldl' nixpkgs.lib.attrsets.recursiveUpdate {}; - stdOutput = accumulate (builtins.concatLists (builtins.map stdOutputsFor Systems)); + stdOutput = accumulate (builtins.map stdOutputsFor Systems); # List of all flake outputs injected by std in the outputs and inputs.cells format - stdOutputsFor = system: builtins.map (loadCell system) Cells; + stdOutputsFor = system: let + acc = accumulate (builtins.map (loadCell system) Cells); + meta = { + # materialize meta & also realize all implicit runtime dependencies + __std.${system} = nixpkgs.legacyPackages.${system}.writeTextFile { + name = "__std-${system}.json"; + # flatten meta for easier ingestion by the std cli + text = builtins.toJSON (builtins.attrValues acc.__std.${system}); + }; + }; + in + # nixpkgs.lib.traceSeqN 4 meta + acc + // meta; # Load a cell, return the flake outputs injected by std loadCell = system: cellName: let cellArgs = { @@ -140,16 +132,16 @@ ); in builtins.foldl' op {} Organelles; - # Each Cell's Organelle can inject a singleton or an attribute set output into the project, not both loadOrganelle = organelle: args: let - path = organellePath cellsFrom cellName organelle; - importedFile = validate.MigrationNecesary path.file (import path.file); - importedDir = validate.MigrationNecesary path.dir (import path.dir); + cPath = paths.cellPath cellsFrom cellName; + oPath = paths.organellePath cPath organelle; + importedFile = validate.FileSignature oPath.file (import oPath.file); + importedDir = validate.FileSignature oPath.dir (import oPath.dir); in - if builtins.pathExists path.file - then validate.Import organelle.clade path.file (importedFile args) - else if builtins.pathExists path.dir - then validate.Import organelle.clade path.dir (importedDir args) + if builtins.pathExists oPath.file + then validate.Import organelle.clade oPath.file (importedFile args) + else if builtins.pathExists oPath.dir + then validate.Import organelle.clade oPath.dir (importedDir args) else {}; # Postprocess the result of the cell loading organelles' = nixpkgs.lib.lists.groupBy (x: x.name) Organelles; @@ -157,20 +149,46 @@ nixpkgs.lib.attrsets.mapAttrsToList ( organelleName: output: let organelle = builtins.head organelles'.${organelleName}; - toStdTypedOutput = name: output: { - __std_name = - output.meta.mainProgram or output.pname or output.name or name; - __std_description = - output.meta.description or output.description or "n/a"; - __std_cell = cellName; - __std_clade = organelle.clade; - __std_organelle = organelle.name; + cPath = paths.cellPath cellsFrom cellName; + oPath = paths.organellePath cPath organelle; + extractStdMeta = name: output: let + tPath = paths.targetPath oPath name; + in { + name = "${cellName}-${organelleName}-${name}"; + value = { + __std_name = name; + __std_description = + output.meta.description or output.description or "n/a"; + __std_cell = cellName; + __std_clade = organelle.clade; + __std_organelle = organelle.name; + __std_readme = + if builtins.pathExists tPath.readme + then tPath.readme + else ""; + __std_cell_readme = + if builtins.pathExists cPath.readme + then cPath.readme + else ""; + __std_organelle_readme = + if builtins.pathExists oPath.readme + then oPath.readme + else ""; + __std_actions = + if organelle ? actions + then + organelle.actions { + inherit system; + flake = inputs.self.sourceInfo.outPath; + fragment = ''"${system}"."${cellName}"."${organelleName}"."${name}"''; + } + else []; + }; }; in { ${system}.${cellName}.${organelleName} = output; # parseable index of targets for tooling - __std.${system}.${cellName}.${organelleName} = - builtins.mapAttrs toStdTypedOutput output; + __std.${system} = nixpkgs.lib.mapAttrs' extractStdMeta output; } ) cell; @@ -254,10 +272,13 @@ in { inherit + (clades) runnables installables functions data + ; + inherit grow growOn harvest @@ -272,9 +293,10 @@ # as-nix-cli-epiphyte = false; cellsFrom = ./cells; organelles = [ - (runnables "cli") - (functions "lib") - (functions "devshellProfiles") + (clades.runnables "cli") + (clades.functions "lib") + (clades.functions "devshellProfiles") + (clades.data "data") ]; systems = ["x86_64-linux"]; } diff --git a/paths.nix b/paths.nix new file mode 100644 index 00000000..5b0b56c2 --- /dev/null +++ b/paths.nix @@ -0,0 +1,15 @@ +{ + cellPath = cellsFrom: cellName: { + __toString = _: "${cellsFrom}/${cellName}"; + readme = "${cellsFrom}/${cellName}/Readme.md"; + }; + organellePath = cellPath: organelle: { + __toString = _: "${cellPath}/${organelle.name}"; + file = "${cellPath}/${organelle.name}.nix"; + dir = "${cellPath}/${organelle.name}/default.nix"; + readme = "${cellPath}/${organelle.name}/Readme.md"; + }; + targetPath = organellePath: name: { + readme = "${organellePath}/${name}.md"; + }; +} diff --git a/treefmt.toml b/treefmt.toml index 9ad7adbf..7df7113a 100644 --- a/treefmt.toml +++ b/treefmt.toml @@ -9,6 +9,12 @@ command = "alejandra" includes = ["*.nix"] + +[formatter.go] +command = "gofmt" +options = ["-w"] +includes = ["*.go"] + [formatter.prettier] command = "prettier" options = ["--write"] diff --git a/validators.nix b/validators.nix index 5a451353..64e2575d 100644 --- a/validators.nix +++ b/validators.nix @@ -5,8 +5,8 @@ { nixpkgs, yants, - organellePath, }: let + inherit (import ./paths.nix) cellPath organellePath; prefixWithCellsFrom = path: builtins.concatStringsSep "/" ( ["\${cellsFrom}"] @@ -16,7 +16,8 @@ in { Systems = with yants "std" "grow" "attrs"; list (enum "system" nixpkgs.lib.systems.doubles.all); Cell = cellsFrom: organelles: cell: type: let - path = o: organellePath cellsFrom cell o; + cPath = cellPath cellsFrom cell; + path = o: organellePath cPath o; atLeastOneOrganelle = builtins.any (x: x) ( builtins.map ( o: builtins.pathExists (path o).file || builtins.pathExists (path o).dir @@ -64,14 +65,25 @@ in { Please create at least one of the previous files and don't forget to add them to version control. '' else cell; - Organelles = with yants "std" "grow" "attrs"; + Organelles = with yants "std" "grow" "attrs"; let + action = struct "action" { + name = string; + description = string; + command = list string; + }; + in list ( struct "organelle" { name = string; clade = enum "clades" ["runnables" "installables" "functions" "data"]; + actions = option (functionWithArgs { + system = false; + flake = false; + fragment = false; + }); } ); - MigrationNecesary = file: let + FileSignature = file: let file' = prefixWithCellsFrom file; in with yants "std" "import" file';