From 2202ca9d3b10ee2cd52190c20567e758bc009dea Mon Sep 17 00:00:00 2001 From: David Arnold Date: Wed, 13 Apr 2022 16:30:12 -0500 Subject: [PATCH 01/27] WIP: std TUI --- cells/std/cli/.gitignore | 1 + cells/std/cli/default.nix | 117 ++++------------------ cells/std/cli/delegate.go | 82 +++++++++++++++ cells/std/cli/go.mod | 10 ++ cells/std/cli/go.sum | 49 +++++++++ cells/std/cli/main.go | 188 +++++++++++++++++++++++++++++++++++ cells/std/cli/randomitems.go | 149 +++++++++++++++++++++++++++ devshell/flake.nix | 17 ++++ treefmt.toml | 6 ++ 9 files changed, 519 insertions(+), 100 deletions(-) create mode 100644 cells/std/cli/.gitignore create mode 100644 cells/std/cli/delegate.go create mode 100644 cells/std/cli/go.mod create mode 100644 cells/std/cli/go.sum create mode 100644 cells/std/cli/main.go create mode 100644 cells/std/cli/randomitems.go 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/default.nix b/cells/std/cli/default.nix index b2228322..47801402 100644 --- a/cells/std/cli/default.nix +++ b/cells/std/cli/default.nix @@ -7,107 +7,24 @@ 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]; - }; + 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.stdenv.mkDerivation { - name = "std"; - meta.description = "nix shortcut for projects that conform to Standard"; + nixpkgs.buildGoModule rec { + inherit version; + pname = "std"; + meta.description = "A tui 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" - ''; + + vendorSha256 = null; + + ldflags = [ + "-s" + "-w" + "-X main.buildVersion=${version}" + "-X main.buildCommit=${commit}" + ]; }; } diff --git a/cells/std/cli/delegate.go b/cells/std/cli/delegate.go new file mode 100644 index 00000000..ef0b0f81 --- /dev/null +++ b/cells/std/cli/delegate.go @@ -0,0 +1,82 @@ +package main + +import ( + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" +) + +func newItemDelegate(keys *delegateKeyMap) list.DefaultDelegate { + d := list.NewDefaultDelegate() + + d.UpdateFunc = func(msg tea.Msg, m *list.Model) tea.Cmd { + var title string + + if i, ok := m.SelectedItem().(item); ok { + title = i.Title() + } else { + return nil + } + + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, keys.choose): + return m.NewStatusMessage(statusMessageStyle("You chose " + title)) + + // case key.Matches(msg, keys.remove): + // index := m.Index() + // m.RemoveItem(index) + // if len(m.Items()) == 0 { + // keys.remove.SetEnabled(false) + // } + // return m.NewStatusMessage(statusMessageStyle("Deleted " + title)) + } + } + + return nil + } + + help := []key.Binding{keys.choose} + + d.ShortHelpFunc = func() []key.Binding { + return help + } + + d.FullHelpFunc = func() [][]key.Binding { + return [][]key.Binding{help} + } + + return d +} + +type delegateKeyMap struct { + choose key.Binding +} + +// Additional short help entries. This satisfies the help.KeyMap interface and +// is entirely optional. +func (d delegateKeyMap) ShortHelp() []key.Binding { + return []key.Binding{ + d.choose, + } +} + +// Additional full help entries. This satisfies the help.KeyMap interface and +// is entirely optional. +func (d delegateKeyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{ + { + d.choose, + }, + } +} + +func newDelegateKeyMap() *delegateKeyMap { + return &delegateKeyMap{ + choose: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "choose"), + ), + } +} diff --git a/cells/std/cli/go.mod b/cells/std/cli/go.mod new file mode 100644 index 00000000..95be2230 --- /dev/null +++ b/cells/std/cli/go.mod @@ -0,0 +1,10 @@ +module github.com/divnix/std/cells/std/cli + +go 1.16 + +require ( + github.com/charmbracelet/bubbles v0.10.3 + github.com/charmbracelet/bubbletea v0.20.0 + github.com/charmbracelet/lipgloss v0.5.0 + github.com/treilik/bubbleboxer v0.1.0 +) diff --git a/cells/std/cli/go.sum b/cells/std/cli/go.sum new file mode 100644 index 00000000..70fab0e0 --- /dev/null +++ b/cells/std/cli/go.sum @@ -0,0 +1,49 @@ +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +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/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/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +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-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.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/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/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/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/treilik/bubbleboxer v0.1.0 h1:xA1dxzKGRDHTUiaj+p1MHWKtIMCpGIqir3IuSla74rw= +github.com/treilik/bubbleboxer v0.1.0/go.mod h1:fbg7Cm6Ex9qkQnN2jN6O/NZN49q8uAyKdmPcb4Y59kc= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/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-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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= diff --git a/cells/std/cli/main.go b/cells/std/cli/main.go new file mode 100644 index 00000000..dcf7afb0 --- /dev/null +++ b/cells/std/cli/main.go @@ -0,0 +1,188 @@ +package main + +import ( + // "encoding/json" + "fmt" + "math/rand" + "os" + "time" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + boxer "github.com/treilik/bubbleboxer" +) + +var ( + appStyle = lipgloss.NewStyle().Padding(1, 2) + + titleStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FFFDF5")). + Background(lipgloss.Color("#25A065")). + Padding(0, 1) + + statusMessageStyle = lipgloss.NewStyle(). + Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#04B575"}). + Render + + targetRender = "//%s/%s:%s" +) + +type item struct { + name string `json:"__std_name"` + organelle string `json:"__std_organelle"` + cell string `json:"__std_cell"` + clade string `json:"__std_clade"` + description string `json:"__std_description"` +} + +func (i item) Title() string { return fmt.Sprintf(targetRender, i.cell, i.organelle, i.name) } +func (i item) Description() string { return i.description } +func (i item) FilterValue() string { return i.Title() } + +type listKeyMap struct { + toggleSpinner key.Binding + toggleTitleBar key.Binding + toggleStatusBar key.Binding + togglePagination key.Binding +} + +func newListKeyMap() *listKeyMap { + return &listKeyMap{ + toggleSpinner: key.NewBinding( + key.WithKeys("s"), + key.WithHelp("s", "toggle spinner"), + ), + toggleTitleBar: key.NewBinding( + key.WithKeys("T"), + key.WithHelp("T", "toggle title"), + ), + toggleStatusBar: key.NewBinding( + key.WithKeys("S"), + key.WithHelp("S", "toggle status"), + ), + togglePagination: key.NewBinding( + key.WithKeys("P"), + key.WithHelp("P", "toggle pagination"), + ), + } +} + +type model struct { + tui boxer.Boxer + list list.Model + itemGenerator *randomItemGenerator + keys *listKeyMap + delegateKeys *delegateKeyMap +} + +func newModel() model { + var ( + itemGenerator randomItemGenerator + delegateKeys = newDelegateKeyMap() + listKeys = newListKeyMap() + ) + + // Make initial list of items + const numItems = 24 + items := make([]list.Item, numItems) + for i := 0; i < numItems; i++ { + items[i] = itemGenerator.next() + } + + // Setup list + delegate := newItemDelegate(delegateKeys) + targetList := list.New(items, delegate, 0, 0) + targetList.Title = "Standard Targets" + targetList.Styles.Title = titleStyle + targetList.AdditionalFullHelpKeys = func() []key.Binding { + return []key.Binding{ + listKeys.toggleSpinner, + listKeys.toggleTitleBar, + listKeys.toggleStatusBar, + listKeys.togglePagination, + } + } + // TODO: read std metadata via nix eval + // stdMetaJson := `[{"__std_name": "name","__std_organelle": "organelle","__std_cell": "cell","__std_clade": "clade","__std_description": "A description ..."}]` + // stdMeta: json.Unmarshal([]byte(stdMetaJson), &stdMeta), + // system: "x86_64-linux", + // stdMeta represents the std metadata + // type stdMeta []struct { + // } + return model{ + list: targetList, + keys: listKeys, + delegateKeys: delegateKeys, + itemGenerator: &itemGenerator, + } +} + +func (m model) Init() tea.Cmd { + return tea.EnterAltScreen +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + h, v := appStyle.GetFrameSize() + m.list.SetSize(msg.Width-h, msg.Height-v) + + case tea.KeyMsg: + // Don't match any of the keys below if we're actively filtering. + if m.list.FilterState() == list.Filtering { + break + } + + switch { + case key.Matches(msg, m.keys.toggleSpinner): + cmd := m.list.ToggleSpinner() + return m, cmd + + case key.Matches(msg, m.keys.toggleTitleBar): + v := !m.list.ShowTitle() + m.list.SetShowTitle(v) + m.list.SetShowFilter(v) + m.list.SetFilteringEnabled(v) + return m, nil + + case key.Matches(msg, m.keys.toggleStatusBar): + m.list.SetShowStatusBar(!m.list.ShowStatusBar()) + return m, nil + + case key.Matches(msg, m.keys.togglePagination): + m.list.SetShowPagination(!m.list.ShowPagination()) + return m, nil + + //case key.Matches(msg, m.keys.insertItem): + // m.delegateKeys.remove.SetEnabled(true) + // newItem := m.itemGenerator.next() + // insCmd := m.list.InsertItem(0, newItem) + // statusCmd := m.list.NewStatusMessage(statusMessageStyle("Added " + newItem.Title())) + // return m, tea.Batch(insCmd, statusCmd) + } + } + + // This will also call our delegate's update function. + newListModel, cmd := m.list.Update(msg) + m.list = newListModel + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +func (m model) View() string { + return appStyle.Render(m.list.View()) +} + +func main() { + rand.Seed(time.Now().UTC().UnixNano()) + + if err := tea.NewProgram(newModel()).Start(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} diff --git a/cells/std/cli/randomitems.go b/cells/std/cli/randomitems.go new file mode 100644 index 00000000..be526fcc --- /dev/null +++ b/cells/std/cli/randomitems.go @@ -0,0 +1,149 @@ +package main + +import ( + "math/rand" + "sync" +) + +type randomItemGenerator struct { + names []string + nameIndex int + organelles []string + organelleIndex int + cells []string + cellIndex int + clades []string + cladeIndex int + descs []string + descIndex 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.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) + }) +} + +func (r *randomItemGenerator) next() item { + if r.mtx == nil { + r.reset() + } + + r.mtx.Lock() + defer r.mtx.Unlock() + + i := item{ + name: r.names[r.nameIndex], + organelle: r.organelles[r.organelleIndex], + cell: r.cells[r.cellIndex], + clade: r.clades[r.cladeIndex], + description: r.descs[r.descIndex], + } + + 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 + } + + return i +} diff --git a/devshell/flake.nix b/devshell/flake.nix index e0301a0a..d5a1c387 100644 --- a/devshell/flake.nix +++ b/devshell/flake.nix @@ -36,6 +36,23 @@ 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"; + } ]; imports = [ "${extraModulesPath}/git/hooks.nix" 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"] From 3ada1949b27f4e70384be0f0b679a1521eb4bfc3 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Thu, 14 Apr 2022 19:30:20 -0500 Subject: [PATCH 02/27] WIP: std TUI --- cells/std/cli/actions.go | 80 +++++++++ cells/std/cli/delegate.go | 82 --------- cells/std/cli/go.mod | 1 - cells/std/cli/go.sum | 3 +- cells/std/cli/keys.go | 90 ++++++++++ cells/std/cli/main.go | 220 +++++++++-------------- cells/std/cli/main.scm | 336 ----------------------------------- cells/std/cli/randomitems.go | 124 +++++++++++++ cells/std/cli/targets.go | 84 +++++++++ 9 files changed, 463 insertions(+), 557 deletions(-) create mode 100644 cells/std/cli/actions.go delete mode 100644 cells/std/cli/delegate.go create mode 100644 cells/std/cli/keys.go delete mode 100644 cells/std/cli/main.scm create mode 100644 cells/std/cli/targets.go diff --git a/cells/std/cli/actions.go b/cells/std/cli/actions.go new file mode 100644 index 00000000..75ca37a2 --- /dev/null +++ b/cells/std/cli/actions.go @@ -0,0 +1,80 @@ +package main + +import ( + "fmt" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +var ( + ActionStyle = lipgloss.NewStyle(). + Width(30).Height(32). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("63")) +) + +type ActionModel struct { + List list.Model +} + +func (m *ActionModel) Init() tea.Cmd { return nil } + +func (m *ActionModel) Update(msg tea.Msg) (*ActionModel, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + } + m.List, cmd = m.List.Update(msg) + return m, cmd + case tea.WindowSizeMsg: + } + return m, cmd +} +func (m *ActionModel) View() string { + return ActionStyle.Render(m.List.View()) +} + +func NewAction(i item) *ActionModel { + var ( + appKeys = NewAppKeyMap() + numItems = cap(i.actions) + ) + // Make list of actions + items := make([]list.Item, numItems) + for j := 0; j < numItems; j++ { + items[j] = i.actions[j] + } + actionList := list.New(items, list.NewDefaultDelegate(), 0, 32) + actionList.Title = fmt.Sprintf("Actions for %s", i.clade) + actionList.KeyMap = DefaultListKeyMap() + actionList.AdditionalShortHelpKeys = func() []key.Binding { + return []key.Binding{ + appKeys.toggleFocus, + } + } + actionList.AdditionalFullHelpKeys = func() []key.Binding { + return []key.Binding{ + appKeys.toggleFocus, + } + } + actionList.SetShowPagination(false) + actionList.SetShowHelp(false) + actionList.SetShowStatusBar(false) + actionList.SetFilteringEnabled(false) + + return &ActionModel{actionList} +} + +type action struct { + name string `json:"__action_name"` + command []string `json:"__action_command"` + description string `json:"__action_description"` +} + +func (a action) Title() string { return a.name } +func (a action) Description() string { return a.description } +func (a action) FilterValue() string { return a.Title() } diff --git a/cells/std/cli/delegate.go b/cells/std/cli/delegate.go deleted file mode 100644 index ef0b0f81..00000000 --- a/cells/std/cli/delegate.go +++ /dev/null @@ -1,82 +0,0 @@ -package main - -import ( - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" -) - -func newItemDelegate(keys *delegateKeyMap) list.DefaultDelegate { - d := list.NewDefaultDelegate() - - d.UpdateFunc = func(msg tea.Msg, m *list.Model) tea.Cmd { - var title string - - if i, ok := m.SelectedItem().(item); ok { - title = i.Title() - } else { - return nil - } - - switch msg := msg.(type) { - case tea.KeyMsg: - switch { - case key.Matches(msg, keys.choose): - return m.NewStatusMessage(statusMessageStyle("You chose " + title)) - - // case key.Matches(msg, keys.remove): - // index := m.Index() - // m.RemoveItem(index) - // if len(m.Items()) == 0 { - // keys.remove.SetEnabled(false) - // } - // return m.NewStatusMessage(statusMessageStyle("Deleted " + title)) - } - } - - return nil - } - - help := []key.Binding{keys.choose} - - d.ShortHelpFunc = func() []key.Binding { - return help - } - - d.FullHelpFunc = func() [][]key.Binding { - return [][]key.Binding{help} - } - - return d -} - -type delegateKeyMap struct { - choose key.Binding -} - -// Additional short help entries. This satisfies the help.KeyMap interface and -// is entirely optional. -func (d delegateKeyMap) ShortHelp() []key.Binding { - return []key.Binding{ - d.choose, - } -} - -// Additional full help entries. This satisfies the help.KeyMap interface and -// is entirely optional. -func (d delegateKeyMap) FullHelp() [][]key.Binding { - return [][]key.Binding{ - { - d.choose, - }, - } -} - -func newDelegateKeyMap() *delegateKeyMap { - return &delegateKeyMap{ - choose: key.NewBinding( - key.WithKeys("enter"), - key.WithHelp("enter", "choose"), - ), - } -} diff --git a/cells/std/cli/go.mod b/cells/std/cli/go.mod index 95be2230..ffb1af74 100644 --- a/cells/std/cli/go.mod +++ b/cells/std/cli/go.mod @@ -6,5 +6,4 @@ require ( github.com/charmbracelet/bubbles v0.10.3 github.com/charmbracelet/bubbletea v0.20.0 github.com/charmbracelet/lipgloss v0.5.0 - github.com/treilik/bubbleboxer v0.1.0 ) diff --git a/cells/std/cli/go.sum b/cells/std/cli/go.sum index 70fab0e0..28b87436 100644 --- a/cells/std/cli/go.sum +++ b/cells/std/cli/go.sum @@ -12,6 +12,7 @@ github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0 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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 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= @@ -37,8 +38,6 @@ 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/treilik/bubbleboxer v0.1.0 h1:xA1dxzKGRDHTUiaj+p1MHWKtIMCpGIqir3IuSla74rw= -github.com/treilik/bubbleboxer v0.1.0/go.mod h1:fbg7Cm6Ex9qkQnN2jN6O/NZN49q8uAyKdmPcb4Y59kc= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/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= diff --git a/cells/std/cli/keys.go b/cells/std/cli/keys.go new file mode 100644 index 00000000..233214ae --- /dev/null +++ b/cells/std/cli/keys.go @@ -0,0 +1,90 @@ +package main + +import ( + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" +) + +type AppKeyMap struct { + toggleFocus key.Binding + forceQuit key.Binding +} + +func NewAppKeyMap() *AppKeyMap { + return &AppKeyMap{ + // Swiching focus. + toggleFocus: key.NewBinding( + key.WithKeys("tab"), + key.WithHelp("⇥", "toggle focus"), + ), + + // Quitting. + forceQuit: key.NewBinding(key.WithKeys("ctrl+c")), + } +} + +// DefaultListKeyMap returns a default set of keybindings. +func DefaultListKeyMap() list.KeyMap { + return list.KeyMap{ + // Browsing. + CursorUp: key.NewBinding( + key.WithKeys("up"), + key.WithHelp("↑", "up"), + ), + CursorDown: key.NewBinding( + key.WithKeys("down"), + key.WithHelp("↓", "down"), + ), + PrevPage: key.NewBinding( + key.WithKeys("pgup"), + key.WithHelp("pgup", "prev page"), + ), + NextPage: key.NewBinding( + key.WithKeys("pgdown"), + key.WithHelp("pgdn", "next page"), + ), + GoToStart: key.NewBinding( + key.WithKeys("home"), + key.WithHelp("home", "go to start"), + ), + GoToEnd: key.NewBinding( + key.WithKeys("end"), + key.WithHelp("end", "go to end"), + ), + Filter: key.NewBinding( + key.WithKeys("/"), + key.WithHelp("/", "filter"), + ), + ClearFilter: key.NewBinding( + key.WithKeys("esc"), + key.WithHelp("esc", "clear filter"), + ), + + // Filtering. + CancelWhileFiltering: key.NewBinding( + key.WithKeys("esc"), + key.WithHelp("esc", "cancel"), + ), + AcceptWhileFiltering: key.NewBinding( + key.WithKeys("enter", "tab", "up", "down"), + key.WithHelp("enter", "apply filter"), + ), + + // Toggle help. + ShowFullHelp: key.NewBinding( + key.WithKeys("?"), + key.WithHelp("?", "more"), + ), + CloseFullHelp: key.NewBinding( + key.WithKeys("?"), + key.WithHelp("?", "close help"), + ), + + // Quitting. + Quit: key.NewBinding( + key.WithKeys("q"), + key.WithHelp("q", "quit"), + ), + ForceQuit: key.NewBinding(key.WithKeys("ctrl+c")), + } +} diff --git a/cells/std/cli/main.go b/cells/std/cli/main.go index dcf7afb0..3a425fb2 100644 --- a/cells/std/cli/main.go +++ b/cells/std/cli/main.go @@ -1,8 +1,8 @@ package main import ( - // "encoding/json" "fmt" + "math/rand" "os" "time" @@ -11,177 +11,125 @@ import ( "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - boxer "github.com/treilik/bubbleboxer" ) +type Focus int64 + +const ( + Left Focus = iota + Right +) + +func (s Focus) String() string { + switch s { + case Left: + return "left focus" + case Right: + return "right focus" + } + return "unknown" +} + var ( - appStyle = lipgloss.NewStyle().Padding(1, 2) + AppStyle = lipgloss.NewStyle(). + BorderForeground(lipgloss.Color("63")). + Padding(1, 2) - titleStyle = lipgloss.NewStyle(). + TitleStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#FFFDF5")). Background(lipgloss.Color("#25A065")). Padding(0, 1) - statusMessageStyle = lipgloss.NewStyle(). + StatusMessageStyle = lipgloss.NewStyle(). Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#04B575"}). Render - - targetRender = "//%s/%s:%s" ) -type item struct { - name string `json:"__std_name"` - organelle string `json:"__std_organelle"` - cell string `json:"__std_cell"` - clade string `json:"__std_clade"` - description string `json:"__std_description"` -} - -func (i item) Title() string { return fmt.Sprintf(targetRender, i.cell, i.organelle, i.name) } -func (i item) Description() string { return i.description } -func (i item) FilterValue() string { return i.Title() } - -type listKeyMap struct { - toggleSpinner key.Binding - toggleTitleBar key.Binding - toggleStatusBar key.Binding - togglePagination key.Binding -} - -func newListKeyMap() *listKeyMap { - return &listKeyMap{ - toggleSpinner: key.NewBinding( - key.WithKeys("s"), - key.WithHelp("s", "toggle spinner"), - ), - toggleTitleBar: key.NewBinding( - key.WithKeys("T"), - key.WithHelp("T", "toggle title"), - ), - toggleStatusBar: key.NewBinding( - key.WithKeys("S"), - key.WithHelp("S", "toggle status"), - ), - togglePagination: key.NewBinding( - key.WithKeys("P"), - key.WithHelp("P", "toggle pagination"), - ), - } +type AppModel struct { + Target *TargetModel + Action *ActionModel + Keys *AppKeyMap + Focus } -type model struct { - tui boxer.Boxer - list list.Model - itemGenerator *randomItemGenerator - keys *listKeyMap - delegateKeys *delegateKeyMap +func (m *AppModel) Init() tea.Cmd { + return tea.EnterAltScreen } - -func newModel() model { +func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var ( - itemGenerator randomItemGenerator - delegateKeys = newDelegateKeyMap() - listKeys = newListKeyMap() + cmds []tea.Cmd + cmd tea.Cmd ) - - // Make initial list of items - const numItems = 24 - items := make([]list.Item, numItems) - for i := 0; i < numItems; i++ { - items[i] = itemGenerator.next() - } - - // Setup list - delegate := newItemDelegate(delegateKeys) - targetList := list.New(items, delegate, 0, 0) - targetList.Title = "Standard Targets" - targetList.Styles.Title = titleStyle - targetList.AdditionalFullHelpKeys = func() []key.Binding { - return []key.Binding{ - listKeys.toggleSpinner, - listKeys.toggleTitleBar, - listKeys.toggleStatusBar, - listKeys.togglePagination, - } - } - // TODO: read std metadata via nix eval - // stdMetaJson := `[{"__std_name": "name","__std_organelle": "organelle","__std_cell": "cell","__std_clade": "clade","__std_description": "A description ..."}]` - // stdMeta: json.Unmarshal([]byte(stdMetaJson), &stdMeta), - // system: "x86_64-linux", - // stdMeta represents the std metadata - // type stdMeta []struct { - // } - return model{ - list: targetList, - keys: listKeys, - delegateKeys: delegateKeys, - itemGenerator: &itemGenerator, - } -} - -func (m model) Init() tea.Cmd { - return tea.EnterAltScreen -} - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmds []tea.Cmd - switch msg := msg.(type) { - case tea.WindowSizeMsg: - h, v := appStyle.GetFrameSize() - m.list.SetSize(msg.Width-h, msg.Height-v) - case tea.KeyMsg: + // quit even during filtering + if key.Matches(msg, m.Keys.forceQuit) { + return m, tea.Quit + } // Don't match any of the keys below if we're actively filtering. - if m.list.FilterState() == list.Filtering { + if m.Target.List.FilterState() == list.Filtering { break } - switch { - case key.Matches(msg, m.keys.toggleSpinner): - cmd := m.list.ToggleSpinner() - return m, cmd - - case key.Matches(msg, m.keys.toggleTitleBar): - v := !m.list.ShowTitle() - m.list.SetShowTitle(v) - m.list.SetShowFilter(v) - m.list.SetFilteringEnabled(v) - return m, nil - - case key.Matches(msg, m.keys.toggleStatusBar): - m.list.SetShowStatusBar(!m.list.ShowStatusBar()) - return m, nil - - case key.Matches(msg, m.keys.togglePagination): - m.list.SetShowPagination(!m.list.ShowPagination()) - return m, nil - - //case key.Matches(msg, m.keys.insertItem): - // m.delegateKeys.remove.SetEnabled(true) - // newItem := m.itemGenerator.next() - // insCmd := m.list.InsertItem(0, newItem) - // statusCmd := m.list.NewStatusMessage(statusMessageStyle("Added " + newItem.Title())) - // return m, tea.Batch(insCmd, statusCmd) + case key.Matches(msg, m.Keys.toggleFocus): + if m.Focus == Left { + m.Focus = Right + cmd = m.Target.List.ToggleSpinner() + cmds = append(cmds, cmd) + cmd = m.Action.List.ToggleSpinner() + cmds = append(cmds, cmd) + } else { + m.Focus = Left + cmd = m.Target.List.ToggleSpinner() + cmds = append(cmds, cmd) + cmd = m.Action.List.ToggleSpinner() + cmds = append(cmds, cmd) + } + return m, tea.Batch(cmds...) } + case tea.WindowSizeMsg: } // This will also call our delegate's update function. - newListModel, cmd := m.list.Update(msg) - m.list = newListModel - cmds = append(cmds, cmd) + if m.Focus == Left { + m.Target, cmd = m.Target.Update(msg) + m.Action = NewAction(m.Target.List.SelectedItem().(item)) + cmds = append(cmds, cmd) + } else { + m.Action, cmd = m.Action.Update(msg) + cmds = append(cmds, cmd) + } return m, tea.Batch(cmds...) } +func (m *AppModel) View() string { + var help string + if m.Focus == Left { + var l = m.Target.List + help = l.Styles.HelpStyle.Render(l.Help.View(l)) + } else { + var l = m.Action.List + help = l.Styles.HelpStyle.Render(l.Help.View(l)) + } + return AppStyle.Render( + lipgloss.JoinVertical( + lipgloss.Center, + lipgloss.JoinHorizontal(lipgloss.Left, m.Target.View(), m.Action.View()), + help, + )) +} -func (m model) View() string { - return appStyle.Render(m.list.View()) +func InitialPage() *AppModel { + targets := InitialTarget() + action := NewAction(targets.List.SelectedItem().(item)) + self := &AppModel{targets, action, NewAppKeyMap(), Left} + return self } func main() { rand.Seed(time.Now().UTC().UnixNano()) - if err := tea.NewProgram(newModel()).Start(); err != nil { + 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/randomitems.go b/cells/std/cli/randomitems.go index be526fcc..ac67798a 100644 --- a/cells/std/cli/randomitems.go +++ b/cells/std/cli/randomitems.go @@ -112,12 +112,23 @@ func (r *randomItemGenerator) next() item { r.mtx.Lock() defer r.mtx.Unlock() + var ( + actionsGenerator randomActionGenerator + ) + // Make actions + const numItems = 3 + items := make([]action, numItems) + for i := 0; i < numItems; i++ { + items[i] = actionsGenerator.next() + } + i := item{ name: r.names[r.nameIndex], organelle: r.organelles[r.organelleIndex], cell: r.cells[r.cellIndex], clade: r.clades[r.cladeIndex], description: r.descs[r.descIndex], + actions: items, } r.nameIndex++ @@ -147,3 +158,116 @@ func (r *randomItemGenerator) next() item { 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{ + []string{"nix", "run", ".#f.r.a.g.m.e.n.t"}, + []string{"nix", "build", ".#fragment", "&&", "nomad", "result/job"}, + []string{"cat", "./cow"}, + []string{"cowsay", "hi"}, + []string{"fastlane", "run", "..."}, + []string{"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() action { + if r.mtx == nil { + r.reset() + } + + r.mtx.Lock() + defer r.mtx.Unlock() + + a := action{ + name: r.actionNames[r.actionNameIndex], + command: r.actionCommands[r.actionCommandIndex], + description: 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/targets.go b/cells/std/cli/targets.go new file mode 100644 index 00000000..32768524 --- /dev/null +++ b/cells/std/cli/targets.go @@ -0,0 +1,84 @@ +package main + +import ( + "fmt" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +const targetTemplate = "//%s/%s:%s" + +var TargetStyle = lipgloss.NewStyle(). + Height(32).Width(60). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("63")) + +type TargetModel struct { + List list.Model +} + +func (m *TargetModel) Init() tea.Cmd { return nil } +func (m *TargetModel) Update(msg tea.Msg) (*TargetModel, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + } + m.List, cmd = m.List.Update(msg) + return m, cmd + case tea.WindowSizeMsg: + } + return m, cmd +} +func (m *TargetModel) View() string { + return TargetStyle.Render(m.List.View()) +} + +func InitialTarget() *TargetModel { + var ( + targetsGenerator randomItemGenerator + appKeys = NewAppKeyMap() + ) + + // Make initial list of items + const numItems = 24 + items := make([]list.Item, numItems) + for i := 0; i < numItems; i++ { + items[i] = targetsGenerator.next() + } + + targetList := list.New(items, list.NewDefaultDelegate(), 60, 32) + targetList.Title = "Target" + targetList.KeyMap = DefaultListKeyMap() + targetList.AdditionalShortHelpKeys = func() []key.Binding { + return []key.Binding{ + appKeys.toggleFocus, + } + } + targetList.AdditionalFullHelpKeys = func() []key.Binding { + return []key.Binding{ + appKeys.toggleFocus, + } + } + targetList.SetShowHelp(false) + targetList.SetFilteringEnabled(true) + targetList.StartSpinner() + + return &TargetModel{targetList} +} + +type item struct { + name string `json:"__std_name"` + organelle string `json:"__std_organelle"` + cell string `json:"__std_cell"` + clade string `json:"__std_clade"` + description string `json:"__std_description"` + actions []action +} + +func (i item) Title() string { return fmt.Sprintf(targetTemplate, i.cell, i.organelle, i.name) } +func (i item) Description() string { return i.description } +func (i item) FilterValue() string { return i.Title() } From 37c99926294747f2dd95de889c26c844aa2d4c66 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Thu, 14 Apr 2022 22:53:36 -0500 Subject: [PATCH 03/27] WIP: fix filter --- cells/std/cli/main.go | 6 +++++- cells/std/cli/targets.go | 9 +-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/cells/std/cli/main.go b/cells/std/cli/main.go index 3a425fb2..41744d6e 100644 --- a/cells/std/cli/main.go +++ b/cells/std/cli/main.go @@ -93,7 +93,11 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // This will also call our delegate's update function. if m.Focus == Left { m.Target, cmd = m.Target.Update(msg) - m.Action = NewAction(m.Target.List.SelectedItem().(item)) + if m.Target.List.SelectedItem() != nil { + m.Action = NewAction(m.Target.List.SelectedItem().(item)) + } else { + m.Action = &ActionModel{} + } cmds = append(cmds, cmd) } else { m.Action, cmd = m.Action.Update(msg) diff --git a/cells/std/cli/targets.go b/cells/std/cli/targets.go index 32768524..b9c530ca 100644 --- a/cells/std/cli/targets.go +++ b/cells/std/cli/targets.go @@ -23,14 +23,7 @@ type TargetModel struct { func (m *TargetModel) Init() tea.Cmd { return nil } func (m *TargetModel) Update(msg tea.Msg) (*TargetModel, tea.Cmd) { var cmd tea.Cmd - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - } - m.List, cmd = m.List.Update(msg) - return m, cmd - case tea.WindowSizeMsg: - } + m.List, cmd = m.List.Update(msg) return m, cmd } func (m *TargetModel) View() string { From 644f3596a4787aba07751042a592e12085bf05d9 Mon Sep 17 00:00:00 2001 From: Michael Fellinger Date: Fri, 15 Apr 2022 12:07:04 +0200 Subject: [PATCH 04/27] fix linting issues --- cells/std/cli/actions.go | 12 ++++++------ cells/std/cli/randomitems.go | 30 +++++++++++++++--------------- cells/std/cli/targets.go | 19 ++++++++++--------- 3 files changed, 31 insertions(+), 30 deletions(-) diff --git a/cells/std/cli/actions.go b/cells/std/cli/actions.go index 75ca37a2..275e6c09 100644 --- a/cells/std/cli/actions.go +++ b/cells/std/cli/actions.go @@ -49,7 +49,7 @@ func NewAction(i item) *ActionModel { items[j] = i.actions[j] } actionList := list.New(items, list.NewDefaultDelegate(), 0, 32) - actionList.Title = fmt.Sprintf("Actions for %s", i.clade) + actionList.Title = fmt.Sprintf("Actions for %s", i.StdClade) actionList.KeyMap = DefaultListKeyMap() actionList.AdditionalShortHelpKeys = func() []key.Binding { return []key.Binding{ @@ -70,11 +70,11 @@ func NewAction(i item) *ActionModel { } type action struct { - name string `json:"__action_name"` - command []string `json:"__action_command"` - description string `json:"__action_description"` + ActionName string `json:"__action_name"` + ActionCommand []string `json:"__action_command"` + ActionDescription string `json:"__action_description"` } -func (a action) Title() string { return a.name } -func (a action) Description() string { return a.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/randomitems.go b/cells/std/cli/randomitems.go index ac67798a..5a5d200b 100644 --- a/cells/std/cli/randomitems.go +++ b/cells/std/cli/randomitems.go @@ -123,12 +123,12 @@ func (r *randomItemGenerator) next() item { } i := item{ - name: r.names[r.nameIndex], - organelle: r.organelles[r.organelleIndex], - cell: r.cells[r.cellIndex], - clade: r.clades[r.cladeIndex], - description: r.descs[r.descIndex], - actions: items, + 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], + actions: items, } r.nameIndex++ @@ -182,12 +182,12 @@ func (r *randomActionGenerator) reset() { "test", } r.actionCommands = [][]string{ - []string{"nix", "run", ".#f.r.a.g.m.e.n.t"}, - []string{"nix", "build", ".#fragment", "&&", "nomad", "result/job"}, - []string{"cat", "./cow"}, - []string{"cowsay", "hi"}, - []string{"fastlane", "run", "..."}, - []string{"go", "build", "."}, + {"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", @@ -249,9 +249,9 @@ func (r *randomActionGenerator) next() action { defer r.mtx.Unlock() a := action{ - name: r.actionNames[r.actionNameIndex], - command: r.actionCommands[r.actionCommandIndex], - description: r.actionDescs[r.actionDescIndex], + ActionName: r.actionNames[r.actionNameIndex], + ActionCommand: r.actionCommands[r.actionCommandIndex], + ActionDescription: r.actionDescs[r.actionDescIndex], } r.actionNameIndex++ diff --git a/cells/std/cli/targets.go b/cells/std/cli/targets.go index b9c530ca..622f9650 100644 --- a/cells/std/cli/targets.go +++ b/cells/std/cli/targets.go @@ -58,20 +58,21 @@ func InitialTarget() *TargetModel { } targetList.SetShowHelp(false) targetList.SetFilteringEnabled(true) - targetList.StartSpinner() return &TargetModel{targetList} } type item struct { - name string `json:"__std_name"` - organelle string `json:"__std_organelle"` - cell string `json:"__std_cell"` - clade string `json:"__std_clade"` - description string `json:"__std_description"` - actions []action + 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"` + actions []action } -func (i item) Title() string { return fmt.Sprintf(targetTemplate, i.cell, i.organelle, i.name) } -func (i item) Description() string { return i.description } +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() } From 1b9e6f2cb5be850cc6c09938b0c71298e70014e4 Mon Sep 17 00:00:00 2001 From: Michael Fellinger Date: Fri, 15 Apr 2022 12:07:18 +0200 Subject: [PATCH 05/27] support j/k for movement --- cells/std/cli/keys.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cells/std/cli/keys.go b/cells/std/cli/keys.go index 233214ae..036aab31 100644 --- a/cells/std/cli/keys.go +++ b/cells/std/cli/keys.go @@ -28,12 +28,12 @@ func DefaultListKeyMap() list.KeyMap { return list.KeyMap{ // Browsing. CursorUp: key.NewBinding( - key.WithKeys("up"), - key.WithHelp("↑", "up"), + key.WithKeys("k", "up"), + key.WithHelp("k/↑", "up"), ), CursorDown: key.NewBinding( - key.WithKeys("down"), - key.WithHelp("↓", "down"), + key.WithKeys("j", "down"), + key.WithHelp("j/↓", "down"), ), PrevPage: key.NewBinding( key.WithKeys("pgup"), From 381cbf81447215e3bee2751adda46133afb70c96 Mon Sep 17 00:00:00 2001 From: Michael Fellinger Date: Fri, 15 Apr 2022 13:05:34 +0200 Subject: [PATCH 06/27] example of showing help --- cells/std/cli/keys.go | 7 +++++++ cells/std/cli/main.go | 48 +++++++++++++++++++++++++++++++++---------- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/cells/std/cli/keys.go b/cells/std/cli/keys.go index 036aab31..d6801e48 100644 --- a/cells/std/cli/keys.go +++ b/cells/std/cli/keys.go @@ -8,6 +8,7 @@ import ( type AppKeyMap struct { toggleFocus key.Binding forceQuit key.Binding + toggleHelp key.Binding } func NewAppKeyMap() *AppKeyMap { @@ -18,6 +19,12 @@ func NewAppKeyMap() *AppKeyMap { key.WithHelp("⇥", "toggle focus"), ), + // Toggle help. + toggleHelp: key.NewBinding( + key.WithKeys("?"), + key.WithHelp("?", "toggle help"), + ), + // Quitting. forceQuit: key.NewBinding(key.WithKeys("ctrl+c")), } diff --git a/cells/std/cli/main.go b/cells/std/cli/main.go index 41744d6e..670bad21 100644 --- a/cells/std/cli/main.go +++ b/cells/std/cli/main.go @@ -50,11 +50,15 @@ type AppModel struct { Action *ActionModel Keys *AppKeyMap Focus + FullHelp bool + Width int + Height int } func (m *AppModel) Init() tea.Cmd { return tea.EnterAltScreen } + func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var ( cmds []tea.Cmd @@ -71,6 +75,8 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { break } switch { + case key.Matches(msg, m.Keys.toggleHelp): + m.FullHelp = !m.FullHelp case key.Matches(msg, m.Keys.toggleFocus): if m.Focus == Left { m.Focus = Right @@ -85,9 +91,14 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmd = m.Action.List.ToggleSpinner() cmds = append(cmds, cmd) } - return m, tea.Batch(cmds...) } case tea.WindowSizeMsg: + m.Width = msg.Width + m.Height = msg.Height + m.Target.List.SetHeight(msg.Height - 10) + m.Target.List.SetWidth(msg.Width / 2) + m.Action.List.SetHeight(msg.Height - 10) + m.Action.List.SetWidth(msg.Width / 2) } // This will also call our delegate's update function. @@ -106,6 +117,7 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Batch(cmds...) } + func (m *AppModel) View() string { var help string if m.Focus == Left { @@ -115,19 +127,33 @@ func (m *AppModel) View() string { var l = m.Action.List help = l.Styles.HelpStyle.Render(l.Help.View(l)) } - return AppStyle.Render( - lipgloss.JoinVertical( - lipgloss.Center, - lipgloss.JoinHorizontal(lipgloss.Left, m.Target.View(), m.Action.View()), - help, - )) + + if m.FullHelp { + return lipgloss.Place(m.Width, m.Height, lipgloss.Center, lipgloss.Center, + lipgloss.NewStyle().Border(lipgloss.NormalBorder()).Padding(1, 2).Render("Help"), + ) + } + + return lipgloss.Place(m.Width, m.Height, lipgloss.Center, lipgloss.Center, + AppStyle.MaxWidth(m.Width).MaxHeight(m.Height).Render( + lipgloss.JoinVertical( + lipgloss.Center, + lipgloss.JoinHorizontal(lipgloss.Left, m.Target.View(), m.Action.View()), + help, + )), + ) } func InitialPage() *AppModel { - targets := InitialTarget() - action := NewAction(targets.List.SelectedItem().(item)) - self := &AppModel{targets, action, NewAppKeyMap(), Left} - return self + target := InitialTarget() + action := NewAction(target.List.SelectedItem().(item)) + return &AppModel{ + Target: target, + Action: action, + Keys: NewAppKeyMap(), + Focus: Left, + FullHelp: false, + } } func main() { From ea47b4c05376db98a12503fe94dd803c3bde6de2 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Sat, 16 Apr 2022 17:41:01 -0500 Subject: [PATCH 07/27] WIP: fix proportions --- cells/std/cli/actions.go | 27 ++++++------------- cells/std/cli/keys.go | 2 +- cells/std/cli/main.go | 57 +++++++++++++++++++++++++++++----------- cells/std/cli/targets.go | 15 +++++------ devshell/flake.nix | 4 +++ 5 files changed, 61 insertions(+), 44 deletions(-) diff --git a/cells/std/cli/actions.go b/cells/std/cli/actions.go index 275e6c09..391410e7 100644 --- a/cells/std/cli/actions.go +++ b/cells/std/cli/actions.go @@ -6,36 +6,23 @@ import ( "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" -) - -var ( - ActionStyle = lipgloss.NewStyle(). - Width(30).Height(32). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("63")) ) type ActionModel struct { - List list.Model + List list.Model + Width int + Height int } func (m *ActionModel) Init() tea.Cmd { return nil } func (m *ActionModel) Update(msg tea.Msg) (*ActionModel, tea.Cmd) { var cmd tea.Cmd - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - } - m.List, cmd = m.List.Update(msg) - return m, cmd - case tea.WindowSizeMsg: - } + m.List, cmd = m.List.Update(msg) return m, cmd } func (m *ActionModel) View() string { - return ActionStyle.Render(m.List.View()) + return m.List.View() } func NewAction(i item) *ActionModel { @@ -66,7 +53,9 @@ func NewAction(i item) *ActionModel { actionList.SetShowStatusBar(false) actionList.SetFilteringEnabled(false) - return &ActionModel{actionList} + return &ActionModel{ + List: actionList, + } } type action struct { diff --git a/cells/std/cli/keys.go b/cells/std/cli/keys.go index d6801e48..d58ea7fa 100644 --- a/cells/std/cli/keys.go +++ b/cells/std/cli/keys.go @@ -15,7 +15,7 @@ func NewAppKeyMap() *AppKeyMap { return &AppKeyMap{ // Swiching focus. toggleFocus: key.NewBinding( - key.WithKeys("tab"), + key.WithKeys("tab", "shift+tab"), key.WithHelp("⇥", "toggle focus"), ), diff --git a/cells/std/cli/main.go b/cells/std/cli/main.go index 670bad21..60ed7a9d 100644 --- a/cells/std/cli/main.go +++ b/cells/std/cli/main.go @@ -35,14 +35,22 @@ var ( BorderForeground(lipgloss.Color("63")). Padding(1, 2) - TitleStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FFFDF5")). - Background(lipgloss.Color("#25A065")). - Padding(0, 1) - - StatusMessageStyle = lipgloss.NewStyle(). - Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#04B575"}). - Render + TargetStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("63")) + + ActionStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("63")) + + // TitleStyle = lipgloss.NewStyle(). + // Foreground(lipgloss.Color("#FFFDF5")). + // Background(lipgloss.Color("#25A065")). + // Padding(0, 1) + + // StatusMessageStyle = lipgloss.NewStyle(). + // Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#04B575"}). + // Render ) type AppModel struct { @@ -95,19 +103,34 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: m.Width = msg.Width m.Height = msg.Height - m.Target.List.SetHeight(msg.Height - 10) - m.Target.List.SetWidth(msg.Width / 2) - m.Action.List.SetHeight(msg.Height - 10) - m.Action.List.SetWidth(msg.Width / 2) + m.Target.Height = msg.Height - 10 + m.Target.Width = msg.Width*2/3 - 10 + m.Action.Height = msg.Height - 10 + m.Action.Width = msg.Width*1/3 - 10 + m.Target.List.SetHeight(m.Target.Height) + m.Target.List.SetWidth(m.Target.Width) + m.Action.List.SetHeight(m.Action.Height) + m.Action.List.SetWidth(m.Action.Width) + return m, nil } // This will also call our delegate's update function. if m.Focus == Left { m.Target, cmd = m.Target.Update(msg) if m.Target.List.SelectedItem() != nil { - m.Action = NewAction(m.Target.List.SelectedItem().(item)) + var ( + target = m.Target.List.SelectedItem().(item) + numItems = cap(target.actions) + ) + // Make list of actions + items := make([]list.Item, numItems) + for j := 0; j < numItems; j++ { + items[j] = target.actions[j] + } + m.Action.List.Title = fmt.Sprintf("Actions for %s", target.StdClade) + m.Action.List.SetItems(items) } else { - m.Action = &ActionModel{} + m.Action.List.SetItems([]list.Item{}) } cmds = append(cmds, cmd) } else { @@ -138,7 +161,11 @@ func (m *AppModel) View() string { AppStyle.MaxWidth(m.Width).MaxHeight(m.Height).Render( lipgloss.JoinVertical( lipgloss.Center, - lipgloss.JoinHorizontal(lipgloss.Left, m.Target.View(), m.Action.View()), + lipgloss.JoinHorizontal( + lipgloss.Left, + TargetStyle.Width(m.Target.Width).Height(m.Target.Height).Render(m.Target.View()), + ActionStyle.Width(m.Action.Width).Height(m.Action.Height).Render(m.Action.View()), + ), help, )), ) diff --git a/cells/std/cli/targets.go b/cells/std/cli/targets.go index 622f9650..43748097 100644 --- a/cells/std/cli/targets.go +++ b/cells/std/cli/targets.go @@ -6,18 +6,14 @@ import ( "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" ) const targetTemplate = "//%s/%s:%s" -var TargetStyle = lipgloss.NewStyle(). - Height(32).Width(60). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("63")) - type TargetModel struct { - List list.Model + List list.Model + Width int + Height int } func (m *TargetModel) Init() tea.Cmd { return nil } @@ -27,7 +23,7 @@ func (m *TargetModel) Update(msg tea.Msg) (*TargetModel, tea.Cmd) { return m, cmd } func (m *TargetModel) View() string { - return TargetStyle.Render(m.List.View()) + return m.List.View() } func InitialTarget() *TargetModel { @@ -58,8 +54,9 @@ func InitialTarget() *TargetModel { } targetList.SetShowHelp(false) targetList.SetFilteringEnabled(true) + targetList.StartSpinner() - return &TargetModel{targetList} + return &TargetModel{List: targetList} } type item struct { diff --git a/devshell/flake.nix b/devshell/flake.nix index d5a1c387..81af39a4 100644 --- a/devshell/flake.nix +++ b/devshell/flake.nix @@ -53,6 +53,10 @@ package = nixpkgs.legacyPackages.gopls; category = "cli-dev"; } + { + package = nixpkgs.legacyPackages.golangci-lint; + category = "cli-dev"; + } ]; imports = [ "${extraModulesPath}/git/hooks.nix" From 659ef722f598331820594aa4e2ecf20dd373acfd Mon Sep 17 00:00:00 2001 From: David Arnold Date: Sun, 17 Apr 2022 01:34:49 -0500 Subject: [PATCH 08/27] WIP: add markdown help --- cells/std/cli/actions.go | 61 +++++++--- cells/std/cli/go.mod | 1 + cells/std/cli/go.sum | 52 ++++++++ cells/std/cli/help.go | 165 +++++++++++++++++++++++++ cells/std/cli/keys.go | 127 ++++++++++---------- cells/std/cli/main.go | 156 +++++++++++++++--------- cells/std/cli/random-readme-1.md | 200 +++++++++++++++++++++++++++++++ cells/std/cli/random-readme-2.md | 199 ++++++++++++++++++++++++++++++ cells/std/cli/randomitems.go | 15 +++ cells/std/cli/targets.go | 71 ++++++++--- 10 files changed, 900 insertions(+), 147 deletions(-) create mode 100644 cells/std/cli/help.go create mode 100644 cells/std/cli/random-readme-1.md create mode 100644 cells/std/cli/random-readme-2.md diff --git a/cells/std/cli/actions.go b/cells/std/cli/actions.go index 391410e7..08e8f046 100644 --- a/cells/std/cli/actions.go +++ b/cells/std/cli/actions.go @@ -6,28 +6,66 @@ import ( "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) type ActionModel struct { + Target *item List list.Model Width int Height int } +func (m *ActionModel) SetTarget(t *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 cmd tea.Cmd + var ( + appKeys = NewAppKeyMap() + cmd tea.Cmd + ) + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, appKeys.ToggleFocus): + 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 m.List.View() + 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(i item) *ActionModel { var ( - appKeys = NewAppKeyMap() numItems = cap(i.actions) ) // Make list of actions @@ -35,27 +73,16 @@ func NewAction(i item) *ActionModel { for j := 0; j < numItems; j++ { items[j] = i.actions[j] } - actionList := list.New(items, list.NewDefaultDelegate(), 0, 32) + actionList := list.New(items, list.NewDefaultDelegate(), 0, 0) actionList.Title = fmt.Sprintf("Actions for %s", i.StdClade) actionList.KeyMap = DefaultListKeyMap() - actionList.AdditionalShortHelpKeys = func() []key.Binding { - return []key.Binding{ - appKeys.toggleFocus, - } - } - actionList.AdditionalFullHelpKeys = func() []key.Binding { - return []key.Binding{ - appKeys.toggleFocus, - } - } actionList.SetShowPagination(false) actionList.SetShowHelp(false) actionList.SetShowStatusBar(false) actionList.SetFilteringEnabled(false) + actionList.DisableQuitKeybindings() - return &ActionModel{ - List: actionList, - } + return &ActionModel{List: actionList} } type action struct { diff --git a/cells/std/cli/go.mod b/cells/std/cli/go.mod index ffb1af74..2b3b0d98 100644 --- a/cells/std/cli/go.mod +++ b/cells/std/cli/go.mod @@ -6,4 +6,5 @@ require ( github.com/charmbracelet/bubbles v0.10.3 github.com/charmbracelet/bubbletea v0.20.0 github.com/charmbracelet/lipgloss v0.5.0 + github.com/knipferrc/teacup v0.0.16 ) diff --git a/cells/std/cli/go.sum b/cells/std/cli/go.sum index 28b87436..c4f8f1ec 100644 --- a/cells/std/cli/go.sum +++ b/cells/std/cli/go.sum @@ -1,10 +1,16 @@ +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/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= @@ -12,19 +18,35 @@ github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0 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/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-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/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= @@ -32,17 +54,47 @@ github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtl 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-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-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/help.go b/cells/std/cli/help.go new file mode 100644 index 00000000..984eb5dc --- /dev/null +++ b/cells/std/cli/help.go @@ -0,0 +1,165 @@ +package main + +import ( + "fmt" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/knipferrc/teacup/markdown" +) + +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/Reame.md +` +) + +type HelpModel struct { + Target *item + TargetHelp markdown.Bubble + CellHelp markdown.Bubble + OrganelleHelp markdown.Bubble + HasTargetHelp bool + HasCellHelp bool + HasOrganelleHelp bool + Active bool + Width int + Height int + KeyMap *HelpKeyMap + Help help.Model + // Focus +} + +func (m *HelpModel) SetTarget(t *item) { + m.Target = t + m.HasTargetHelp = t.StdReadme != "" + m.HasCellHelp = false + m.HasOrganelleHelp = false + 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 ...", "TODO-CELL")) + } 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 ...", "TODO-ORGANELLE")) + } 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 NewHelp() *HelpModel { + 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 = ViewportKeyMap() + ch.Viewport.KeyMap = ViewportKeyMap() + oh.Viewport.KeyMap = ViewportKeyMap() + return &HelpModel{ + TargetHelp: th, + CellHelp: ch, + OrganelleHelp: oh, + Help: help.New(), + KeyMap: NewHelpKeyMap(), + } +} +func (m *HelpModel) Init() tea.Cmd { + return nil +} + +func (m *HelpModel) RenderMarkdown() tea.Cmd { + var ( + cmds []tea.Cmd + cmd tea.Cmd + ) + m.TargetHelp.SetIsActive(true) + if m.HasTargetHelp { + cmd = m.TargetHelp.SetFileName(m.Target.StdReadme) + cmds = append(cmds, cmd) + } + if m.HasCellHelp { + cmd = m.CellHelp.SetFileName(m.Target.StdReadme) + cmds = append(cmds, cmd) + } + if m.HasOrganelleHelp { + cmd = m.OrganelleHelp.SetFileName(m.Target.StdReadme) + cmds = append(cmds, cmd) + } + return tea.Batch(cmds...) +} + +func (m *HelpModel) Update(msg tea.Msg) (*HelpModel, 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.CloseHelp): + m.Active = false + m.TargetHelp.SetIsActive(false) + return m, nil + } + case tea.WindowSizeMsg: + m.TargetHelp.SetSize(m.Width, m.Height) + } + m.TargetHelp, cmd = m.TargetHelp.Update(msg) + return m, cmd +} + +func (m *HelpModel) View() string { + return m.TargetHelp.View() +} + +func (m *HelpModel) ShortHelp() []key.Binding { + kb := []key.Binding{ + m.KeyMap.Up, + m.KeyMap.Down, + m.KeyMap.HalfPageUp, + m.KeyMap.HalfPageDown, + m.KeyMap.CloseHelp, + } + return kb +} + +func (m *HelpModel) FullHelp() [][]key.Binding { + kb := [][]key.Binding{{}} + return kb +} diff --git a/cells/std/cli/keys.go b/cells/std/cli/keys.go index d58ea7fa..11db5090 100644 --- a/cells/std/cli/keys.go +++ b/cells/std/cli/keys.go @@ -3,71 +3,81 @@ package main 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")) + halfPageUp = key.NewBinding(key.WithKeys("left"), key.WithHelp("←", "½ back")) + halfPageDown = 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")) + search = key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "filter")) + showHelp = key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "help")) + closeHelp = key.NewBinding(key.WithKeys("?", "esc"), key.WithHelp("?", "close help")) + 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")) ) type AppKeyMap struct { - toggleFocus key.Binding - forceQuit key.Binding - toggleHelp key.Binding + ToggleFocus key.Binding + ShowHelp key.Binding + Quit key.Binding + ForceQuit key.Binding } func NewAppKeyMap() *AppKeyMap { return &AppKeyMap{ - // Swiching focus. - toggleFocus: key.NewBinding( - key.WithKeys("tab", "shift+tab"), - key.WithHelp("⇥", "toggle focus"), - ), + ToggleFocus: toggleFocus, + ShowHelp: showHelp, + ForceQuit: forceQuit, + Quit: quit, + } +} - // Toggle help. - toggleHelp: key.NewBinding( - key.WithKeys("?"), - key.WithHelp("?", "toggle help"), - ), +type HelpKeyMap struct { + viewport.KeyMap + ShowHelp key.Binding + CloseHelp key.Binding +} - // Quitting. - forceQuit: key.NewBinding(key.WithKeys("ctrl+c")), +func NewHelpKeyMap() *HelpKeyMap { + m := &HelpKeyMap{ + ShowHelp: showHelp, + CloseHelp: closeHelp, } + m.PageDown = pageUp + m.PageUp = pageDown + m.HalfPageUp = halfPageUp + m.HalfPageDown = halfPageDown + m.Up = cursorUp + m.Down = cursorDown + return m } // DefaultListKeyMap returns a default set of keybindings. func DefaultListKeyMap() list.KeyMap { return list.KeyMap{ // Browsing. - CursorUp: key.NewBinding( - key.WithKeys("k", "up"), - key.WithHelp("k/↑", "up"), - ), - CursorDown: key.NewBinding( - key.WithKeys("j", "down"), - key.WithHelp("j/↓", "down"), - ), - PrevPage: key.NewBinding( - key.WithKeys("pgup"), - key.WithHelp("pgup", "prev page"), - ), - NextPage: key.NewBinding( - key.WithKeys("pgdown"), - key.WithHelp("pgdn", "next page"), - ), - GoToStart: key.NewBinding( - key.WithKeys("home"), - key.WithHelp("home", "go to start"), - ), - GoToEnd: key.NewBinding( - key.WithKeys("end"), - key.WithHelp("end", "go to end"), - ), - Filter: key.NewBinding( - key.WithKeys("/"), - key.WithHelp("/", "filter"), - ), + 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"), ), - - // Filtering. CancelWhileFiltering: key.NewBinding( key.WithKeys("esc"), key.WithHelp("esc", "cancel"), @@ -76,22 +86,17 @@ func DefaultListKeyMap() list.KeyMap { key.WithKeys("enter", "tab", "up", "down"), key.WithHelp("enter", "apply filter"), ), + } +} - // Toggle help. - ShowFullHelp: key.NewBinding( - key.WithKeys("?"), - key.WithHelp("?", "more"), - ), - CloseFullHelp: key.NewBinding( - key.WithKeys("?"), - key.WithHelp("?", "close help"), - ), - - // Quitting. - Quit: key.NewBinding( - key.WithKeys("q"), - key.WithHelp("q", "quit"), - ), - ForceQuit: key.NewBinding(key.WithKeys("ctrl+c")), +// ViewportKeyMap returns a set of pager-like default keybindings. +func ViewportKeyMap() viewport.KeyMap { + return viewport.KeyMap{ + PageDown: pageUp, + PageUp: pageDown, + HalfPageUp: halfPageUp, + HalfPageDown: halfPageDown, + Up: cursorUp, + Down: cursorDown, } } diff --git a/cells/std/cli/main.go b/cells/std/cli/main.go index 60ed7a9d..e6ebe3a1 100644 --- a/cells/std/cli/main.go +++ b/cells/std/cli/main.go @@ -7,6 +7,7 @@ import ( "os" "time" + "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" @@ -43,6 +44,12 @@ var ( BorderStyle(lipgloss.NormalBorder()). BorderForeground(lipgloss.Color("63")) + HelpStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.AdaptiveColor{Light: "63", Dark: "63"}) + + LegendStyle = lipgloss.NewStyle().Padding(1, 0, 0, 2) + // TitleStyle = lipgloss.NewStyle(). // Foreground(lipgloss.Color("#FFFDF5")). // Background(lipgloss.Color("#25A065")). @@ -56,11 +63,12 @@ var ( type AppModel struct { Target *TargetModel Action *ActionModel + Help *HelpModel Keys *AppKeyMap + Legend help.Model Focus - FullHelp bool - Width int - Height int + Width int + Height int } func (m *AppModel) Init() tea.Cmd { @@ -75,64 +83,77 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: // quit even during filtering - if key.Matches(msg, m.Keys.forceQuit) { + if key.Matches(msg, m.Keys.ForceQuit) { return m, tea.Quit } // 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 { - case key.Matches(msg, m.Keys.toggleHelp): - m.FullHelp = !m.FullHelp - case key.Matches(msg, m.Keys.toggleFocus): + // toggle the help + case key.Matches(msg, m.Keys.ShowHelp): + // set here to ignore if unselected + if !m.Help.Active { + m.Help.Active = true + cmd = m.Help.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.Help.Active { + break + } if m.Focus == Left { m.Focus = Right - cmd = m.Target.List.ToggleSpinner() - cmds = append(cmds, cmd) - cmd = m.Action.List.ToggleSpinner() - cmds = append(cmds, cmd) } else { m.Focus = Left - cmd = m.Target.List.ToggleSpinner() - cmds = append(cmds, cmd) - cmd = m.Action.List.ToggleSpinner() - cmds = append(cmds, cmd) } + 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.Target.List.SetHeight(m.Target.Height) - m.Target.List.SetWidth(m.Target.Width) - m.Action.List.SetHeight(m.Action.Height) - m.Action.List.SetWidth(m.Action.Width) + m.Action, _ = m.Action.Update(msg) + // size Help + m.Help.Height = msg.Height - 10 + m.Help.Width = msg.Width - 20 + m.Help, _ = m.Help.Update(msg) return m, nil } - - // This will also call our delegate's update function. - if m.Focus == Left { + // route all other messages according to state + if m.Help.Active { + m.Help, cmd = m.Help.Update(msg) + cmds = append(cmds, cmd) + } else if m.Focus == Left { m.Target, cmd = m.Target.Update(msg) - if m.Target.List.SelectedItem() != nil { - var ( - target = m.Target.List.SelectedItem().(item) - numItems = cap(target.actions) - ) - // Make list of actions - items := make([]list.Item, numItems) - for j := 0; j < numItems; j++ { - items[j] = target.actions[j] - } - m.Action.List.Title = fmt.Sprintf("Actions for %s", target.StdClade) - m.Action.List.SetItems(items) + cmds = append(cmds, cmd) + if m.Target.SelectedItem() != nil { + var target = m.Target.SelectedItem() + m.Help.SetTarget(target) + m.Action.SetTarget(target) } else { m.Action.List.SetItems([]list.Item{}) } - cmds = append(cmds, cmd) } else { m.Action, cmd = m.Action.Update(msg) cmds = append(cmds, cmd) @@ -142,18 +163,14 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m *AppModel) View() string { - var help string - if m.Focus == Left { - var l = m.Target.List - help = l.Styles.HelpStyle.Render(l.Help.View(l)) - } else { - var l = m.Action.List - help = l.Styles.HelpStyle.Render(l.Help.View(l)) - } - - if m.FullHelp { + if m.Help.Active { return lipgloss.Place(m.Width, m.Height, lipgloss.Center, lipgloss.Center, - lipgloss.NewStyle().Border(lipgloss.NormalBorder()).Padding(1, 2).Render("Help"), + AppStyle.MaxWidth(m.Width).MaxHeight(m.Height).Render( + lipgloss.JoinVertical( + lipgloss.Center, + HelpStyle.Render(m.Help.View()), + LegendStyle.Render(m.Legend.View(m)), + )), ) } @@ -163,23 +180,54 @@ func (m *AppModel) View() string { lipgloss.Center, lipgloss.JoinHorizontal( lipgloss.Left, - TargetStyle.Width(m.Target.Width).Height(m.Target.Height).Render(m.Target.View()), - ActionStyle.Width(m.Action.Width).Height(m.Action.Height).Render(m.Action.View()), + TargetStyle.Render(m.Target.View()), + ActionStyle.Render(m.Action.View()), ), - help, + LegendStyle.Render(m.Legend.View(m)), )), ) } +func (m *AppModel) ShortHelp() []key.Binding { + if m.Help.Active { + return append(m.Help.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.ShowHelp, + m.Keys.Quit, + }...) + } + } else { + return append(m.Action.ShortHelp(), []key.Binding{ + m.Keys.ToggleFocus, + m.Keys.ShowHelp, + m.Keys.Quit, + }...) + } +} + +func (m *AppModel) FullHelp() [][]key.Binding { + kb := [][]key.Binding{{}} + return kb +} + func InitialPage() *AppModel { target := InitialTarget() action := NewAction(target.List.SelectedItem().(item)) return &AppModel{ - Target: target, - Action: action, - Keys: NewAppKeyMap(), - Focus: Left, - FullHelp: false, + Target: target, + Action: action, + Keys: NewAppKeyMap(), + Focus: Left, + Help: NewHelp(), + Legend: help.New(), } } diff --git a/cells/std/cli/random-readme-1.md b/cells/std/cli/random-readme-1.md new file mode 100644 index 00000000..95c9c999 --- /dev/null +++ b/cells/std/cli/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/random-readme-2.md b/cells/std/cli/random-readme-2.md new file mode 100644 index 00000000..729e1983 --- /dev/null +++ b/cells/std/cli/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/randomitems.go b/cells/std/cli/randomitems.go index 5a5d200b..ddf6393d 100644 --- a/cells/std/cli/randomitems.go +++ b/cells/std/cli/randomitems.go @@ -16,6 +16,8 @@ type randomItemGenerator struct { cladeIndex int descs []string descIndex int + readmes []string + readmeIndex int mtx *sync.Mutex shuffle *sync.Once } @@ -92,6 +94,12 @@ func (r *randomItemGenerator) reset() { "Sure, why not?", } + r.readmes = []string{ + "./random-readme-1.md", + "./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] }) @@ -101,6 +109,7 @@ func (r *randomItemGenerator) reset() { shuf(r.organelles) shuf(r.clades) shuf(r.descs) + shuf(r.readmes) }) } @@ -128,6 +137,7 @@ func (r *randomItemGenerator) next() item { StdCell: r.cells[r.cellIndex], StdClade: r.clades[r.cladeIndex], StdDescription: r.descs[r.descIndex], + StdReadme: r.readmes[r.readmeIndex], actions: items, } @@ -156,6 +166,11 @@ func (r *randomItemGenerator) next() item { r.descIndex = 0 } + r.readmeIndex++ + if r.readmeIndex >= len(r.readmes) { + r.readmeIndex = 0 + } + return i } diff --git a/cells/std/cli/targets.go b/cells/std/cli/targets.go index 43748097..02f76d42 100644 --- a/cells/std/cli/targets.go +++ b/cells/std/cli/targets.go @@ -6,6 +6,7 @@ import ( "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) const targetTemplate = "//%s/%s:%s" @@ -18,18 +19,32 @@ type TargetModel struct { func (m *TargetModel) Init() tea.Cmd { return nil } func (m *TargetModel) Update(msg tea.Msg) (*TargetModel, tea.Cmd) { - var cmd tea.Cmd + var ( + appKeys = NewAppKeyMap() + cmd tea.Cmd + ) + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, appKeys.ToggleFocus): + 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 m.List.View() + return lipgloss.NewStyle().Width(m.Width).Height(m.Height).Render(m.List.View()) } func InitialTarget() *TargetModel { var ( targetsGenerator randomItemGenerator - appKeys = NewAppKeyMap() ) // Make initial list of items @@ -39,32 +54,48 @@ func InitialTarget() *TargetModel { items[i] = targetsGenerator.next() } - targetList := list.New(items, list.NewDefaultDelegate(), 60, 32) + targetList := list.New(items, list.NewDefaultDelegate(), 0, 0) targetList.Title = "Target" targetList.KeyMap = DefaultListKeyMap() - targetList.AdditionalShortHelpKeys = func() []key.Binding { - return []key.Binding{ - appKeys.toggleFocus, - } - } - targetList.AdditionalFullHelpKeys = func() []key.Binding { - return []key.Binding{ - appKeys.toggleFocus, - } - } - targetList.SetShowHelp(false) targetList.SetFilteringEnabled(true) targetList.StartSpinner() + targetList.DisableQuitKeybindings() + targetList.SetShowHelp(false) return &TargetModel{List: targetList} } +func (m *TargetModel) SelectedItem() *item { + if m.List.SelectedItem() == nil { + return nil + } + var i = m.List.SelectedItem().(item) + return &i +} + +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 +} + 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"` + StdReadme string `json:"__std_readme"` actions []action } @@ -73,3 +104,13 @@ func (i item) Title() string { } func (i item) Description() string { return i.StdDescription } func (i item) FilterValue() string { return i.Title() } + +func (i item) GetActionItems() []list.Item { + var numItems = cap(i.actions) + // Make list of actions + items := make([]list.Item, numItems) + for j := 0; j < numItems; j++ { + items[j] = i.actions[j] + } + return items +} From 0e9392cd55ae085820d6e0803e05e8e56fd82a6a Mon Sep 17 00:00:00 2001 From: David Arnold Date: Sun, 17 Apr 2022 01:49:01 -0500 Subject: [PATCH 09/27] few renamings --- cells/std/cli/keys.go | 20 ++++++------- cells/std/cli/main.go | 43 ++++++++++++++-------------- cells/std/cli/{help.go => readme.go} | 28 +++++++++--------- 3 files changed, 44 insertions(+), 47 deletions(-) rename cells/std/cli/{help.go => readme.go} (85%) diff --git a/cells/std/cli/keys.go b/cells/std/cli/keys.go index 11db5090..e2980bde 100644 --- a/cells/std/cli/keys.go +++ b/cells/std/cli/keys.go @@ -18,8 +18,8 @@ var ( home = key.NewBinding(key.WithKeys("home"), key.WithHelp("home", "go to start")) end = key.NewBinding(key.WithKeys("end"), key.WithHelp("end", "go to end")) search = key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "filter")) - showHelp = key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "help")) - closeHelp = key.NewBinding(key.WithKeys("?", "esc"), key.WithHelp("?", "close help")) + showReadme = key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "readme")) + closeReadme = key.NewBinding(key.WithKeys("?", "esc"), key.WithHelp("?", "close readme")) 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")) @@ -27,7 +27,7 @@ var ( type AppKeyMap struct { ToggleFocus key.Binding - ShowHelp key.Binding + ShowReadme key.Binding Quit key.Binding ForceQuit key.Binding } @@ -35,22 +35,20 @@ type AppKeyMap struct { func NewAppKeyMap() *AppKeyMap { return &AppKeyMap{ ToggleFocus: toggleFocus, - ShowHelp: showHelp, + ShowReadme: showReadme, ForceQuit: forceQuit, Quit: quit, } } -type HelpKeyMap struct { +type ReadmeKeyMap struct { viewport.KeyMap - ShowHelp key.Binding - CloseHelp key.Binding + CloseReadme key.Binding } -func NewHelpKeyMap() *HelpKeyMap { - m := &HelpKeyMap{ - ShowHelp: showHelp, - CloseHelp: closeHelp, +func NewReadmeKeyMap() *ReadmeKeyMap { + m := &ReadmeKeyMap{ + CloseReadme: closeReadme, } m.PageDown = pageUp m.PageUp = pageDown diff --git a/cells/std/cli/main.go b/cells/std/cli/main.go index e6ebe3a1..40d6ff27 100644 --- a/cells/std/cli/main.go +++ b/cells/std/cli/main.go @@ -44,7 +44,7 @@ var ( BorderStyle(lipgloss.NormalBorder()). BorderForeground(lipgloss.Color("63")) - HelpStyle = lipgloss.NewStyle(). + ReadmeStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). BorderForeground(lipgloss.AdaptiveColor{Light: "63", Dark: "63"}) @@ -63,7 +63,7 @@ var ( type AppModel struct { Target *TargetModel Action *ActionModel - Help *HelpModel + Readme *ReadmeModel Keys *AppKeyMap Legend help.Model Focus @@ -99,17 +99,16 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } switch { // toggle the help - case key.Matches(msg, m.Keys.ShowHelp): - // set here to ignore if unselected - if !m.Help.Active { - m.Help.Active = true - cmd = m.Help.RenderMarkdown() + case key.Matches(msg, m.Keys.ShowReadme): + 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.Help.Active { + if m.Readme.Active { break } if m.Focus == Left { @@ -134,22 +133,22 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.Action.Height = msg.Height - 10 m.Action.Width = msg.Width*1/3 - 10 m.Action, _ = m.Action.Update(msg) - // size Help - m.Help.Height = msg.Height - 10 - m.Help.Width = msg.Width - 20 - m.Help, _ = m.Help.Update(msg) + // size Readme + m.Readme.Height = msg.Height - 10 + m.Readme.Width = msg.Width - 20 + m.Readme, _ = m.Readme.Update(msg) return m, nil } // route all other messages according to state - if m.Help.Active { - m.Help, cmd = m.Help.Update(msg) + 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.Help.SetTarget(target) + m.Readme.SetTarget(target) m.Action.SetTarget(target) } else { m.Action.List.SetItems([]list.Item{}) @@ -163,12 +162,12 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m *AppModel) View() string { - if m.Help.Active { + if m.Readme.Active { return lipgloss.Place(m.Width, m.Height, lipgloss.Center, lipgloss.Center, AppStyle.MaxWidth(m.Width).MaxHeight(m.Height).Render( lipgloss.JoinVertical( lipgloss.Center, - HelpStyle.Render(m.Help.View()), + ReadmeStyle.Render(m.Readme.View()), LegendStyle.Render(m.Legend.View(m)), )), ) @@ -189,8 +188,8 @@ func (m *AppModel) View() string { } func (m *AppModel) ShortHelp() []key.Binding { - if m.Help.Active { - return append(m.Help.ShortHelp(), []key.Binding{ + if m.Readme.Active { + return append(m.Readme.ShortHelp(), []key.Binding{ m.Keys.Quit, }...) } @@ -200,14 +199,14 @@ func (m *AppModel) ShortHelp() []key.Binding { } else { return append(m.Target.ShortHelp(), []key.Binding{ m.Keys.ToggleFocus, - m.Keys.ShowHelp, + m.Keys.ShowReadme, m.Keys.Quit, }...) } } else { return append(m.Action.ShortHelp(), []key.Binding{ m.Keys.ToggleFocus, - m.Keys.ShowHelp, + m.Keys.ShowReadme, m.Keys.Quit, }...) } @@ -226,7 +225,7 @@ func InitialPage() *AppModel { Action: action, Keys: NewAppKeyMap(), Focus: Left, - Help: NewHelp(), + Readme: NewReadme(), Legend: help.New(), } } diff --git a/cells/std/cli/help.go b/cells/std/cli/readme.go similarity index 85% rename from cells/std/cli/help.go rename to cells/std/cli/readme.go index 984eb5dc..5ff80aa1 100644 --- a/cells/std/cli/help.go +++ b/cells/std/cli/readme.go @@ -31,7 +31,7 @@ To create one, simply drop a file in: ` ) -type HelpModel struct { +type ReadmeModel struct { Target *item TargetHelp markdown.Bubble CellHelp markdown.Bubble @@ -42,12 +42,12 @@ type HelpModel struct { Active bool Width int Height int - KeyMap *HelpKeyMap + KeyMap *ReadmeKeyMap Help help.Model // Focus } -func (m *HelpModel) SetTarget(t *item) { +func (m *ReadmeModel) SetTarget(t *item) { m.Target = t m.HasTargetHelp = t.StdReadme != "" m.HasCellHelp = false @@ -81,7 +81,7 @@ func (m *HelpModel) SetTarget(t *item) { } } -func NewHelp() *HelpModel { +func NewReadme() *ReadmeModel { var ( th = markdown.New(false, true, lipgloss.AdaptiveColor{}) ch = markdown.New(false, true, lipgloss.AdaptiveColor{}) @@ -90,19 +90,19 @@ func NewHelp() *HelpModel { th.Viewport.KeyMap = ViewportKeyMap() ch.Viewport.KeyMap = ViewportKeyMap() oh.Viewport.KeyMap = ViewportKeyMap() - return &HelpModel{ + return &ReadmeModel{ TargetHelp: th, CellHelp: ch, OrganelleHelp: oh, Help: help.New(), - KeyMap: NewHelpKeyMap(), + KeyMap: NewReadmeKeyMap(), } } -func (m *HelpModel) Init() tea.Cmd { +func (m *ReadmeModel) Init() tea.Cmd { return nil } -func (m *HelpModel) RenderMarkdown() tea.Cmd { +func (m *ReadmeModel) RenderMarkdown() tea.Cmd { var ( cmds []tea.Cmd cmd tea.Cmd @@ -123,7 +123,7 @@ func (m *HelpModel) RenderMarkdown() tea.Cmd { return tea.Batch(cmds...) } -func (m *HelpModel) Update(msg tea.Msg) (*HelpModel, tea.Cmd) { +func (m *ReadmeModel) Update(msg tea.Msg) (*ReadmeModel, tea.Cmd) { var ( cmd tea.Cmd ) @@ -132,7 +132,7 @@ func (m *HelpModel) Update(msg tea.Msg) (*HelpModel, tea.Cmd) { switch { // activate and deactivate help // ShowHelp shadows CloseHelp in case of the toggle key '?' - case key.Matches(msg, m.KeyMap.CloseHelp): + case key.Matches(msg, m.KeyMap.CloseReadme): m.Active = false m.TargetHelp.SetIsActive(false) return m, nil @@ -144,22 +144,22 @@ func (m *HelpModel) Update(msg tea.Msg) (*HelpModel, tea.Cmd) { return m, cmd } -func (m *HelpModel) View() string { +func (m *ReadmeModel) View() string { return m.TargetHelp.View() } -func (m *HelpModel) ShortHelp() []key.Binding { +func (m *ReadmeModel) ShortHelp() []key.Binding { kb := []key.Binding{ m.KeyMap.Up, m.KeyMap.Down, m.KeyMap.HalfPageUp, m.KeyMap.HalfPageDown, - m.KeyMap.CloseHelp, + m.KeyMap.CloseReadme, } return kb } -func (m *HelpModel) FullHelp() [][]key.Binding { +func (m *ReadmeModel) FullHelp() [][]key.Binding { kb := [][]key.Binding{{}} return kb } From 3742e82b6ee80e9ea839dbb68c35914da29aea5d Mon Sep 17 00:00:00 2001 From: David Arnold Date: Sun, 17 Apr 2022 03:09:33 -0500 Subject: [PATCH 10/27] readme with tabs --- cells/std/cli/keys.go | 38 +++++----- cells/std/cli/main.go | 15 ++-- cells/std/cli/readme.go | 154 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 174 insertions(+), 33 deletions(-) diff --git a/cells/std/cli/keys.go b/cells/std/cli/keys.go index e2980bde..6afff7aa 100644 --- a/cells/std/cli/keys.go +++ b/cells/std/cli/keys.go @@ -9,20 +9,22 @@ import ( 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")) - halfPageUp = key.NewBinding(key.WithKeys("left"), key.WithHelp("←", "½ back")) - halfPageDown = 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")) - search = key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "filter")) - showReadme = key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "readme")) - closeReadme = key.NewBinding(key.WithKeys("?", "esc"), key.WithHelp("?", "close readme")) - 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")) + cursorUp = key.NewBinding(key.WithKeys("k", "up"), key.WithHelp("k/↑", "up")) + cursorDown = key.NewBinding(key.WithKeys("j", "down"), key.WithHelp("j/↓", "down")) + halfPageUp = key.NewBinding(key.WithKeys("left"), key.WithHelp("←", "½ back")) + halfPageDown = 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")) + search = key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "filter")) + showReadme = key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "readme")) + closeReadme = key.NewBinding(key.WithKeys("?", "esc"), key.WithHelp("?", "close readme")) + 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 { @@ -43,12 +45,16 @@ func NewAppKeyMap() *AppKeyMap { type ReadmeKeyMap struct { viewport.KeyMap - CloseReadme key.Binding + CloseReadme key.Binding + CycleTab key.Binding + ReverseCycleTab key.Binding } func NewReadmeKeyMap() *ReadmeKeyMap { m := &ReadmeKeyMap{ - CloseReadme: closeReadme, + CloseReadme: closeReadme, + CycleTab: cycleTab, + ReverseCycleTab: reverseCycleTab, } m.PageDown = pageUp m.PageUp = pageDown diff --git a/cells/std/cli/main.go b/cells/std/cli/main.go index 40d6ff27..dfc62a86 100644 --- a/cells/std/cli/main.go +++ b/cells/std/cli/main.go @@ -32,21 +32,20 @@ func (s Focus) String() string { } var ( - AppStyle = lipgloss.NewStyle(). - BorderForeground(lipgloss.Color("63")). - Padding(1, 2) + Highlight = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"} + AppStyle = lipgloss.NewStyle().Padding(1, 2) TargetStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("63")) + BorderForeground(Highlight) ActionStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("63")) + BorderForeground(Highlight) ReadmeStyle = lipgloss.NewStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.AdaptiveColor{Light: "63", Dark: "63"}) + // BorderStyle(lipgloss.NormalBorder()). + BorderForeground(Highlight) LegendStyle = lipgloss.NewStyle().Padding(1, 0, 0, 2) @@ -135,7 +134,7 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.Action, _ = m.Action.Update(msg) // size Readme m.Readme.Height = msg.Height - 10 - m.Readme.Width = msg.Width - 20 + m.Readme.Width = msg.Width - 10 m.Readme, _ = m.Readme.Update(msg) return m, nil } diff --git a/cells/std/cli/readme.go b/cells/std/cli/readme.go index 5ff80aa1..2a789cbe 100644 --- a/cells/std/cli/readme.go +++ b/cells/std/cli/readme.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "strings" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" @@ -27,10 +28,49 @@ To create one, simply drop a file in: To create one, simply drop a file in: - ${cellsFrom}/%s/%s/Reame.md + ${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(Highlight). + Padding(0, 1) + + activeTab = tab.Copy().Border(activeTabBorder, true) + + tabGap = tab.Copy(). + BorderTop(false). + BorderLeft(false). + BorderRight(false) +) + type ReadmeModel struct { Target *item TargetHelp markdown.Bubble @@ -47,6 +87,16 @@ type ReadmeModel struct { // Focus } +type renderCellMarkdownMsg struct { + msg tea.Msg +} +type renderOrganelleMarkdownMsg struct { + msg tea.Msg +} +type renderTargetMarkdownMsg struct { + msg tea.Msg +} + func (m *ReadmeModel) SetTarget(t *item) { m.Target = t m.HasTargetHelp = t.StdReadme != "" @@ -108,16 +158,24 @@ func (m *ReadmeModel) RenderMarkdown() tea.Cmd { cmd tea.Cmd ) m.TargetHelp.SetIsActive(true) - if m.HasTargetHelp { - cmd = m.TargetHelp.SetFileName(m.Target.StdReadme) - cmds = append(cmds, cmd) - } if m.HasCellHelp { - cmd = m.CellHelp.SetFileName(m.Target.StdReadme) + cmd = func() tea.Msg { + // TODO: get actual cell-readme + return renderCellMarkdownMsg{m.CellHelp.SetFileName(m.Target.StdReadme)()} + } cmds = append(cmds, cmd) } if m.HasOrganelleHelp { - cmd = m.OrganelleHelp.SetFileName(m.Target.StdReadme) + cmd = func() tea.Msg { + // TODO: get actual organelle-readme + return renderOrganelleMarkdownMsg{m.OrganelleHelp.SetFileName(m.Target.StdReadme)()} + } + 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...) @@ -134,18 +192,89 @@ func (m *ReadmeModel) Update(msg tea.Msg) (*ReadmeModel, tea.Cmd) { // 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) } - m.TargetHelp, cmd = m.TargetHelp.Update(msg) return m, cmd } func (m *ReadmeModel) View() string { - return m.TargetHelp.View() + // 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 { @@ -154,6 +283,7 @@ func (m *ReadmeModel) ShortHelp() []key.Binding { m.KeyMap.Down, m.KeyMap.HalfPageUp, m.KeyMap.HalfPageDown, + m.KeyMap.CycleTab, m.KeyMap.CloseReadme, } return kb @@ -163,3 +293,9 @@ func (m *ReadmeModel) FullHelp() [][]key.Binding { kb := [][]key.Binding{{}} return kb } +func max(a, b int) int { + if a > b { + return a + } + return b +} From 4315e7814b530cd1e4f086083cb0c0f4f0542ca7 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 18 Apr 2022 11:50:09 -0500 Subject: [PATCH 11/27] add loading spinner & test uninitialized state --- cells/std/cli/actions.go | 23 +++++----- cells/std/cli/main.go | 98 +++++++++++++++++++++++++++++++--------- cells/std/cli/targets.go | 12 +---- 3 files changed, 89 insertions(+), 44 deletions(-) diff --git a/cells/std/cli/actions.go b/cells/std/cli/actions.go index 08e8f046..546eb318 100644 --- a/cells/std/cli/actions.go +++ b/cells/std/cli/actions.go @@ -64,17 +64,10 @@ func (m *ActionModel) FullHelp() [][]key.Binding { return kb } -func NewAction(i item) *ActionModel { - var ( - numItems = cap(i.actions) - ) - // Make list of actions - items := make([]list.Item, numItems) - for j := 0; j < numItems; j++ { - items[j] = i.actions[j] - } - actionList := list.New(items, list.NewDefaultDelegate(), 0, 0) - actionList.Title = fmt.Sprintf("Actions for %s", i.StdClade) +func NewAction() *ActionModel { + + actionList := list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) + actionList.Title = fmt.Sprintf("Actions") actionList.KeyMap = DefaultListKeyMap() actionList.SetShowPagination(false) actionList.SetShowHelp(false) @@ -85,6 +78,14 @@ func NewAction(i item) *ActionModel { return &ActionModel{List: actionList} } +func (m *ActionModel) SelectedItem() *action { + if m.List.SelectedItem() == nil { + return nil + } + var i = m.List.SelectedItem().(action) + return &i +} + type action struct { ActionName string `json:"__action_name"` ActionCommand []string `json:"__action_command"` diff --git a/cells/std/cli/main.go b/cells/std/cli/main.go index dfc62a86..defb9ebc 100644 --- a/cells/std/cli/main.go +++ b/cells/std/cli/main.go @@ -10,6 +10,7 @@ import ( "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" ) @@ -21,6 +22,8 @@ const ( Right ) +const cmdTemplate = "std %s %s" + func (s Focus) String() string { switch s { case Left: @@ -49,29 +52,30 @@ var ( LegendStyle = lipgloss.NewStyle().Padding(1, 0, 0, 2) - // TitleStyle = lipgloss.NewStyle(). - // Foreground(lipgloss.Color("#FFFDF5")). - // Background(lipgloss.Color("#25A065")). - // Padding(0, 1) - - // StatusMessageStyle = lipgloss.NewStyle(). - // Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#04B575"}). - // Render + TitleStyle = lipgloss.NewStyle(). + Foreground(Highlight).Bold(true). + Padding(1, 1) ) type AppModel struct { - Target *TargetModel - Action *ActionModel - Readme *ReadmeModel - Keys *AppKeyMap - Legend help.Model + Target *TargetModel + Action *ActionModel + Readme *ReadmeModel + Keys *AppKeyMap + Legend help.Model + Title string + Spinner spinner.Model + Loading bool Focus Width int Height int } func (m *AppModel) Init() tea.Cmd { - return tea.EnterAltScreen + return tea.Batch( + tea.EnterAltScreen, + m.Spinner.Tick, + ) } func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -79,7 +83,19 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds []tea.Cmd cmd tea.Cmd ) + // As soon as targets are loaded, stop the loading spinner + if m.Target.SelectedItem() != nil { + m.Loading = false + } switch msg := msg.(type) { + 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) { @@ -156,16 +172,26 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 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 { + m.Title = fmt.Sprintf(cmdTemplate, m.Target.SelectedItem().Title(), m.Action.SelectedItem().Title()) + } return m, tea.Batch(cmds...) } func (m *AppModel) View() string { + var title string + if m.Loading { + title = TitleStyle.Inline(true).Render("Loading") + " " + TitleStyle.Inline(true).Render(m.Spinner.View()) + } else { + title = TitleStyle.Render(m.Title) + } if m.Readme.Active { return lipgloss.Place(m.Width, m.Height, lipgloss.Center, lipgloss.Center, AppStyle.MaxWidth(m.Width).MaxHeight(m.Height).Render( lipgloss.JoinVertical( lipgloss.Center, + title, ReadmeStyle.Render(m.Readme.View()), LegendStyle.Render(m.Legend.View(m)), )), @@ -176,6 +202,7 @@ func (m *AppModel) View() string { AppStyle.MaxWidth(m.Width).MaxHeight(m.Height).Render( lipgloss.JoinVertical( lipgloss.Center, + title, lipgloss.JoinHorizontal( lipgloss.Left, TargetStyle.Render(m.Target.View()), @@ -217,15 +244,42 @@ func (m *AppModel) FullHelp() [][]key.Binding { } func InitialPage() *AppModel { + // var ( + // numItems = cap(i.actions) + // ) + // // Make list of actions + // items := make([]list.Item, numItems) + // for j := 0; j < numItems; j++ { + // items[j] = i.actions[j] + // } + // actionList := list.New(items, list.NewDefaultDelegate(), 0, 0) + + // var ( + // targetsGenerator randomItemGenerator + // ) + + // // Make initial list of items + // const numItems = 24 + // items := make([]list.Item, numItems) + // for i := 0; i < numItems; i++ { + // items[i] = targetsGenerator.next() + // } + + // targetList := list.New(items, list.NewDefaultDelegate(), 0, 0) + target := InitialTarget() - action := NewAction(target.List.SelectedItem().(item)) + action := NewAction() + spin := spinner.New() + spin.Spinner = spinner.Points return &AppModel{ - Target: target, - Action: action, - Keys: NewAppKeyMap(), - Focus: Left, - Readme: NewReadme(), - Legend: help.New(), + Target: target, + Action: action, + Keys: NewAppKeyMap(), + Focus: Left, + Readme: NewReadme(), + Legend: help.New(), + Loading: true, + Spinner: spin, } } diff --git a/cells/std/cli/targets.go b/cells/std/cli/targets.go index 02f76d42..63b1e732 100644 --- a/cells/std/cli/targets.go +++ b/cells/std/cli/targets.go @@ -43,18 +43,8 @@ func (m *TargetModel) View() string { } func InitialTarget() *TargetModel { - var ( - targetsGenerator randomItemGenerator - ) - - // Make initial list of items - const numItems = 24 - items := make([]list.Item, numItems) - for i := 0; i < numItems; i++ { - items[i] = targetsGenerator.next() - } - targetList := list.New(items, list.NewDefaultDelegate(), 0, 0) + targetList := list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) targetList.Title = "Target" targetList.KeyMap = DefaultListKeyMap() targetList.SetFilteringEnabled(true) From 98f42c684e6063db620821b27aa330da5b242993 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 18 Apr 2022 12:04:55 -0500 Subject: [PATCH 12/27] prepare for loading the flake metadata --- cells/std/cli/flake.go | 36 ++++++++++++++++++++++++++++++++++++ cells/std/cli/main.go | 31 +++++++++---------------------- cells/std/cli/targets.go | 10 ++++++++++ 3 files changed, 55 insertions(+), 22 deletions(-) create mode 100644 cells/std/cli/flake.go diff --git a/cells/std/cli/flake.go b/cells/std/cli/flake.go new file mode 100644 index 00000000..22713cec --- /dev/null +++ b/cells/std/cli/flake.go @@ -0,0 +1,36 @@ +package main + +import ( + // "fmt" + + tea "github.com/charmbracelet/bubbletea" +) + +func loadFlake() tea.Msg { + var ( + targetsGenerator randomItemGenerator + ) + + // // Make list of actions + // items := make([]list.Item, numItems) + // for j := 0; j < numItems; j++ { + // items[j] = i.actions[j] + // } + // Make initial list of items + const numItems = 24 + items := make([]item, numItems) + for i := 0; i < numItems; i++ { + items[i] = targetsGenerator.next() + } + return flakeLoadedMsg{ + Items: items, + } +} + +type flakeLoadedMsg struct { + Items []item +} + +type errMsg struct{ err error } + +func (e errMsg) Error() string { return e.err.Error() } diff --git a/cells/std/cli/main.go b/cells/std/cli/main.go index defb9ebc..a4e21337 100644 --- a/cells/std/cli/main.go +++ b/cells/std/cli/main.go @@ -73,6 +73,7 @@ type AppModel struct { func (m *AppModel) Init() tea.Cmd { return tea.Batch( + loadFlake, tea.EnterAltScreen, m.Spinner.Tick, ) @@ -88,6 +89,14 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.Loading = false } switch msg := msg.(type) { + + case flakeLoadedMsg: + m.Target.SetItems(msg.Items) + return m, nil + + case errMsg: + return m, tea.Quit + case spinner.TickMsg: if m.Loading { m.Spinner, cmd = m.Spinner.Update(msg) @@ -244,28 +253,6 @@ func (m *AppModel) FullHelp() [][]key.Binding { } func InitialPage() *AppModel { - // var ( - // numItems = cap(i.actions) - // ) - // // Make list of actions - // items := make([]list.Item, numItems) - // for j := 0; j < numItems; j++ { - // items[j] = i.actions[j] - // } - // actionList := list.New(items, list.NewDefaultDelegate(), 0, 0) - - // var ( - // targetsGenerator randomItemGenerator - // ) - - // // Make initial list of items - // const numItems = 24 - // items := make([]list.Item, numItems) - // for i := 0; i < numItems; i++ { - // items[i] = targetsGenerator.next() - // } - - // targetList := list.New(items, list.NewDefaultDelegate(), 0, 0) target := InitialTarget() action := NewAction() diff --git a/cells/std/cli/targets.go b/cells/std/cli/targets.go index 63b1e732..d62d852e 100644 --- a/cells/std/cli/targets.go +++ b/cells/std/cli/targets.go @@ -63,6 +63,16 @@ func (m *TargetModel) SelectedItem() *item { return &i } +func (m *TargetModel) SetItems(l []item) { + var numItems = cap(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) } From 2b8a56bc1e7d1b26bee402c1ffd25cab562264d8 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 18 Apr 2022 14:28:04 -0500 Subject: [PATCH 13/27] add left/right for switching focus --- cells/std/cli/actions.go | 2 +- cells/std/cli/flake.go | 26 +++++++++++++------------- cells/std/cli/keys.go | 16 ++++++++++------ cells/std/cli/main.go | 27 +++++++++++++++++++++++++++ cells/std/cli/targets.go | 2 +- 5 files changed, 52 insertions(+), 21 deletions(-) diff --git a/cells/std/cli/actions.go b/cells/std/cli/actions.go index 546eb318..dc9f8963 100644 --- a/cells/std/cli/actions.go +++ b/cells/std/cli/actions.go @@ -32,7 +32,7 @@ func (m *ActionModel) Update(msg tea.Msg) (*ActionModel, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch { - case key.Matches(msg, appKeys.ToggleFocus): + case key.Matches(msg, appKeys.ToggleFocus), key.Matches(msg, appKeys.FocusLeft), key.Matches(msg, appKeys.FocusRight): cmd = m.List.ToggleSpinner() return m, cmd } diff --git a/cells/std/cli/flake.go b/cells/std/cli/flake.go index 22713cec..7f5f3872 100644 --- a/cells/std/cli/flake.go +++ b/cells/std/cli/flake.go @@ -1,29 +1,29 @@ package main import ( - // "fmt" + "encoding/json" tea "github.com/charmbracelet/bubbletea" ) -func loadFlake() tea.Msg { - var ( - targetsGenerator randomItemGenerator - ) - - // // Make list of actions - // items := make([]list.Item, numItems) - // for j := 0; j < numItems; j++ { - // items[j] = i.actions[j] - // } - // Make initial list of items +func fakeData() []item { + var targetsGenerator randomItemGenerator const numItems = 24 items := make([]item, numItems) for i := 0; i < numItems; i++ { items[i] = targetsGenerator.next() } + return items +} + +func loadFlake() tea.Msg { + var items []item + + json.Unmarshal([]byte(``), &items) + return flakeLoadedMsg{ - Items: items, + Items: fakeData(), + // Items: items, } } diff --git a/cells/std/cli/keys.go b/cells/std/cli/keys.go index 6afff7aa..6265ece2 100644 --- a/cells/std/cli/keys.go +++ b/cells/std/cli/keys.go @@ -11,8 +11,8 @@ 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")) - halfPageUp = key.NewBinding(key.WithKeys("left"), key.WithHelp("←", "½ back")) - halfPageDown = key.NewBinding(key.WithKeys("right"), key.WithHelp("→", "½ forward")) + 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")) @@ -29,6 +29,8 @@ var ( type AppKeyMap struct { ToggleFocus key.Binding + FocusLeft key.Binding + FocusRight key.Binding ShowReadme key.Binding Quit key.Binding ForceQuit key.Binding @@ -37,6 +39,8 @@ type AppKeyMap struct { func NewAppKeyMap() *AppKeyMap { return &AppKeyMap{ ToggleFocus: toggleFocus, + FocusLeft: cursorLeft, + FocusRight: cursorRight, ShowReadme: showReadme, ForceQuit: forceQuit, Quit: quit, @@ -58,8 +62,8 @@ func NewReadmeKeyMap() *ReadmeKeyMap { } m.PageDown = pageUp m.PageUp = pageDown - m.HalfPageUp = halfPageUp - m.HalfPageDown = halfPageDown + m.HalfPageUp = cursorLeft + m.HalfPageDown = cursorRight m.Up = cursorUp m.Down = cursorDown return m @@ -98,8 +102,8 @@ func ViewportKeyMap() viewport.KeyMap { return viewport.KeyMap{ PageDown: pageUp, PageUp: pageDown, - HalfPageUp: halfPageUp, - HalfPageDown: halfPageDown, + HalfPageUp: cursorLeft, + HalfPageDown: cursorRight, Up: cursorUp, Down: cursorDown, } diff --git a/cells/std/cli/main.go b/cells/std/cli/main.go index a4e21337..79d7659d 100644 --- a/cells/std/cli/main.go +++ b/cells/std/cli/main.go @@ -145,6 +145,32 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.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 @@ -258,6 +284,7 @@ func InitialPage() *AppModel { action := NewAction() spin := spinner.New() spin.Spinner = spinner.Points + return &AppModel{ Target: target, Action: action, diff --git a/cells/std/cli/targets.go b/cells/std/cli/targets.go index d62d852e..2de4ca4b 100644 --- a/cells/std/cli/targets.go +++ b/cells/std/cli/targets.go @@ -26,7 +26,7 @@ func (m *TargetModel) Update(msg tea.Msg) (*TargetModel, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch { - case key.Matches(msg, appKeys.ToggleFocus): + case key.Matches(msg, appKeys.ToggleFocus), key.Matches(msg, appKeys.FocusLeft), key.Matches(msg, appKeys.FocusRight): cmd = m.List.ToggleSpinner() return m, cmd } From 306ec0ae5d46e38feb750d6eccab402671c9abc4 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 18 Apr 2022 16:30:34 -0500 Subject: [PATCH 14/27] organize files --- cells/std/cli/data/data.go | 45 +++ .../cli/{ => dummy_data}/random-readme-1.md | 0 .../cli/{ => dummy_data}/random-readme-2.md | 0 cells/std/cli/{ => dummy_data}/randomitems.go | 36 +-- cells/std/cli/flake.go | 45 ++- cells/std/cli/{ => keys}/keys.go | 2 +- cells/std/cli/main.go | 287 ------------------ cells/std/cli/{ => models}/actions.go | 27 +- cells/std/cli/{ => models}/readme.go | 22 +- cells/std/cli/{ => models}/targets.go | 45 +-- cells/std/cli/styles/styles.go | 28 ++ cells/std/cli/tui.go | 275 +++++++++++++++++ flake.lock | 48 +++ 13 files changed, 486 insertions(+), 374 deletions(-) create mode 100644 cells/std/cli/data/data.go rename cells/std/cli/{ => dummy_data}/random-readme-1.md (100%) rename cells/std/cli/{ => dummy_data}/random-readme-2.md (100%) rename cells/std/cli/{ => dummy_data}/randomitems.go (87%) rename cells/std/cli/{ => keys}/keys.go (99%) rename cells/std/cli/{ => models}/actions.go (75%) rename cells/std/cli/{ => models}/readme.go (93%) rename cells/std/cli/{ => models}/targets.go (64%) create mode 100644 cells/std/cli/styles/styles.go create mode 100644 cells/std/cli/tui.go create mode 100644 flake.lock diff --git a/cells/std/cli/data/data.go b/cells/std/cli/data/data.go new file mode 100644 index 00000000..0e5c6fac --- /dev/null +++ b/cells/std/cli/data/data.go @@ -0,0 +1,45 @@ +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"` + StdReadme string `json:"__std_readme"` + StdCladeActions []Action +} + +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 = cap(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:"__action_name"` + ActionCommand []string `json:"__action_command"` + ActionDescription string `json:"__action_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/random-readme-1.md b/cells/std/cli/dummy_data/random-readme-1.md similarity index 100% rename from cells/std/cli/random-readme-1.md rename to cells/std/cli/dummy_data/random-readme-1.md diff --git a/cells/std/cli/random-readme-2.md b/cells/std/cli/dummy_data/random-readme-2.md similarity index 100% rename from cells/std/cli/random-readme-2.md rename to cells/std/cli/dummy_data/random-readme-2.md diff --git a/cells/std/cli/randomitems.go b/cells/std/cli/dummy_data/randomitems.go similarity index 87% rename from cells/std/cli/randomitems.go rename to cells/std/cli/dummy_data/randomitems.go index ddf6393d..26e2aa54 100644 --- a/cells/std/cli/randomitems.go +++ b/cells/std/cli/dummy_data/randomitems.go @@ -1,11 +1,13 @@ -package main +package dummy_data import ( "math/rand" "sync" + + "github.com/divnix/std/cells/std/cli/data" ) -type randomItemGenerator struct { +type RandomItemGenerator struct { names []string nameIndex int organelles []string @@ -22,7 +24,7 @@ type randomItemGenerator struct { shuffle *sync.Once } -func (r *randomItemGenerator) reset() { +func (r *RandomItemGenerator) reset() { r.mtx = &sync.Mutex{} r.shuffle = &sync.Once{} @@ -95,8 +97,8 @@ func (r *randomItemGenerator) reset() { } r.readmes = []string{ - "./random-readme-1.md", - "./random-readme-2.md", + "./dummy_data/random-readme-1.md", + "./dummy_data/random-readme-2.md", "", } @@ -113,7 +115,7 @@ func (r *randomItemGenerator) reset() { }) } -func (r *randomItemGenerator) next() item { +func (r *RandomItemGenerator) Next() data.Item { if r.mtx == nil { r.reset() } @@ -126,19 +128,19 @@ func (r *randomItemGenerator) next() item { ) // Make actions const numItems = 3 - items := make([]action, numItems) + items := make([]data.Action, numItems) for i := 0; i < numItems; i++ { items[i] = actionsGenerator.next() } - i := 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], - actions: items, + 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++ @@ -255,7 +257,7 @@ func (r *randomActionGenerator) reset() { } -func (r *randomActionGenerator) next() action { +func (r *randomActionGenerator) next() data.Action { if r.mtx == nil { r.reset() } @@ -263,7 +265,7 @@ func (r *randomActionGenerator) next() action { r.mtx.Lock() defer r.mtx.Unlock() - a := action{ + a := data.Action{ ActionName: r.actionNames[r.actionNameIndex], ActionCommand: r.actionCommands[r.actionCommandIndex], ActionDescription: r.actionDescs[r.actionDescIndex], diff --git a/cells/std/cli/flake.go b/cells/std/cli/flake.go index 7f5f3872..bf94aff4 100644 --- a/cells/std/cli/flake.go +++ b/cells/std/cli/flake.go @@ -2,24 +2,55 @@ package main import ( "encoding/json" + "fmt" + "log" + "os/exec" tea "github.com/charmbracelet/bubbletea" + + "github.com/divnix/std/cells/std/cli/data" + "github.com/divnix/std/cells/std/cli/dummy_data" +) + +var ( + currentSystemArgs = []string{"eval", "--raw", "--impure", "--expr", "builtins.currentSystem"} + flakeStdMetaFragment = "%s#__std.%s" + flakeStdMetaArgs = []string{"eval", "--json", "--option", "warn-dirty", "false"} ) -func fakeData() []item { - var targetsGenerator randomItemGenerator +func fakeData() []data.Item { + var targetsGenerator dummy_data.RandomItemGenerator const numItems = 24 - items := make([]item, numItems) + items := make([]data.Item, numItems) for i := 0; i < numItems; i++ { - items[i] = targetsGenerator.next() + items[i] = targetsGenerator.Next() } return items } func loadFlake() tea.Msg { - var items []item + 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 { + log.Fatal(err) + } + + flakeStdMetaFragment = fmt.Sprintf(flakeStdMetaFragment, ".", currentSystem) + flakeStdMetaArgs = append(flakeStdMetaArgs, flakeStdMetaFragment) + + // load the std metadata from the flake + flakeStdMeta, err := exec.Command(nix, flakeStdMetaArgs...).Output() + if err != nil { + log.Fatal(err) + } - json.Unmarshal([]byte(``), &items) + json.Unmarshal(flakeStdMeta, &items) return flakeLoadedMsg{ Items: fakeData(), @@ -28,7 +59,7 @@ func loadFlake() tea.Msg { } type flakeLoadedMsg struct { - Items []item + Items []data.Item } type errMsg struct{ err error } diff --git a/cells/std/cli/keys.go b/cells/std/cli/keys/keys.go similarity index 99% rename from cells/std/cli/keys.go rename to cells/std/cli/keys/keys.go index 6265ece2..b93981ef 100644 --- a/cells/std/cli/keys.go +++ b/cells/std/cli/keys/keys.go @@ -1,4 +1,4 @@ -package main +package keys import ( "github.com/charmbracelet/bubbles/key" diff --git a/cells/std/cli/main.go b/cells/std/cli/main.go index 79d7659d..7e06fcf4 100644 --- a/cells/std/cli/main.go +++ b/cells/std/cli/main.go @@ -7,296 +7,9 @@ import ( "os" "time" - "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" ) -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" -} - -var ( - Highlight = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"} - AppStyle = lipgloss.NewStyle().Padding(1, 2) - - TargetStyle = lipgloss.NewStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(Highlight) - - 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) -) - -type AppModel struct { - Target *TargetModel - Action *ActionModel - Readme *ReadmeModel - Keys *AppKeyMap - Legend help.Model - Title string - Spinner spinner.Model - Loading bool - Focus - Width int - Height int -} - -func (m *AppModel) Init() tea.Cmd { - return tea.Batch( - loadFlake, - tea.EnterAltScreen, - m.Spinner.Tick, - ) -} - -func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var ( - cmds []tea.Cmd - cmd tea.Cmd - ) - // 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 errMsg: - return m, tea.Quit - - 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 - } - // 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.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 { - m.Title = fmt.Sprintf(cmdTemplate, m.Target.SelectedItem().Title(), m.Action.SelectedItem().Title()) - } - return m, tea.Batch(cmds...) -} - -func (m *AppModel) View() string { - var title string - if m.Loading { - title = TitleStyle.Inline(true).Render("Loading") + " " + TitleStyle.Inline(true).Render(m.Spinner.View()) - } else { - title = TitleStyle.Render(m.Title) - } - if m.Readme.Active { - return lipgloss.Place(m.Width, m.Height, lipgloss.Center, lipgloss.Center, - AppStyle.MaxWidth(m.Width).MaxHeight(m.Height).Render( - lipgloss.JoinVertical( - lipgloss.Center, - title, - ReadmeStyle.Render(m.Readme.View()), - LegendStyle.Render(m.Legend.View(m)), - )), - ) - } - - return lipgloss.Place(m.Width, m.Height, lipgloss.Center, lipgloss.Center, - AppStyle.MaxWidth(m.Width).MaxHeight(m.Height).Render( - lipgloss.JoinVertical( - lipgloss.Center, - title, - lipgloss.JoinHorizontal( - lipgloss.Left, - TargetStyle.Render(m.Target.View()), - ActionStyle.Render(m.Action.View()), - ), - LegendStyle.Render(m.Legend.View(m)), - )), - ) -} - -func (m *AppModel) 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 *AppModel) FullHelp() [][]key.Binding { - kb := [][]key.Binding{{}} - return kb -} - -func InitialPage() *AppModel { - - target := InitialTarget() - action := NewAction() - spin := spinner.New() - spin.Spinner = spinner.Points - - return &AppModel{ - Target: target, - Action: action, - Keys: NewAppKeyMap(), - Focus: Left, - Readme: NewReadme(), - Legend: help.New(), - Loading: true, - Spinner: spin, - } -} - func main() { rand.Seed(time.Now().UTC().UnixNano()) diff --git a/cells/std/cli/actions.go b/cells/std/cli/models/actions.go similarity index 75% rename from cells/std/cli/actions.go rename to cells/std/cli/models/actions.go index dc9f8963..36d76164 100644 --- a/cells/std/cli/actions.go +++ b/cells/std/cli/models/actions.go @@ -1,4 +1,4 @@ -package main +package models import ( "fmt" @@ -7,16 +7,19 @@ import ( "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 ActionModel struct { - Target *item + Target *data.Item List list.Model Width int Height int } -func (m *ActionModel) SetTarget(t *item) { +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()) @@ -26,7 +29,7 @@ func (m *ActionModel) Init() tea.Cmd { return nil } func (m *ActionModel) Update(msg tea.Msg) (*ActionModel, tea.Cmd) { var ( - appKeys = NewAppKeyMap() + appKeys = keys.NewAppKeyMap() cmd tea.Cmd ) switch msg := msg.(type) { @@ -68,7 +71,7 @@ func NewAction() *ActionModel { actionList := list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) actionList.Title = fmt.Sprintf("Actions") - actionList.KeyMap = DefaultListKeyMap() + actionList.KeyMap = keys.DefaultListKeyMap() actionList.SetShowPagination(false) actionList.SetShowHelp(false) actionList.SetShowStatusBar(false) @@ -78,20 +81,10 @@ func NewAction() *ActionModel { return &ActionModel{List: actionList} } -func (m *ActionModel) SelectedItem() *action { +func (m *ActionModel) SelectedItem() *data.Action { if m.List.SelectedItem() == nil { return nil } - var i = m.List.SelectedItem().(action) + var i = m.List.SelectedItem().(data.Action) return &i } - -type action struct { - ActionName string `json:"__action_name"` - ActionCommand []string `json:"__action_command"` - ActionDescription string `json:"__action_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/readme.go b/cells/std/cli/models/readme.go similarity index 93% rename from cells/std/cli/readme.go rename to cells/std/cli/models/readme.go index 2a789cbe..89c65e0f 100644 --- a/cells/std/cli/readme.go +++ b/cells/std/cli/models/readme.go @@ -1,4 +1,4 @@ -package main +package models import ( "fmt" @@ -9,6 +9,10 @@ import ( 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 ( @@ -60,7 +64,7 @@ var ( tab = lipgloss.NewStyle(). Border(tabBorder, true). - BorderForeground(Highlight). + BorderForeground(styles.Highlight). Padding(0, 1) activeTab = tab.Copy().Border(activeTabBorder, true) @@ -72,7 +76,7 @@ var ( ) type ReadmeModel struct { - Target *item + Target *data.Item TargetHelp markdown.Bubble CellHelp markdown.Bubble OrganelleHelp markdown.Bubble @@ -82,7 +86,7 @@ type ReadmeModel struct { Active bool Width int Height int - KeyMap *ReadmeKeyMap + KeyMap *keys.ReadmeKeyMap Help help.Model // Focus } @@ -97,7 +101,7 @@ type renderTargetMarkdownMsg struct { msg tea.Msg } -func (m *ReadmeModel) SetTarget(t *item) { +func (m *ReadmeModel) SetTarget(t *data.Item) { m.Target = t m.HasTargetHelp = t.StdReadme != "" m.HasCellHelp = false @@ -137,15 +141,15 @@ func NewReadme() *ReadmeModel { ch = markdown.New(false, true, lipgloss.AdaptiveColor{}) oh = markdown.New(false, true, lipgloss.AdaptiveColor{}) ) - th.Viewport.KeyMap = ViewportKeyMap() - ch.Viewport.KeyMap = ViewportKeyMap() - oh.Viewport.KeyMap = ViewportKeyMap() + 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: NewReadmeKeyMap(), + KeyMap: keys.NewReadmeKeyMap(), } } func (m *ReadmeModel) Init() tea.Cmd { diff --git a/cells/std/cli/targets.go b/cells/std/cli/models/targets.go similarity index 64% rename from cells/std/cli/targets.go rename to cells/std/cli/models/targets.go index 2de4ca4b..919549b8 100644 --- a/cells/std/cli/targets.go +++ b/cells/std/cli/models/targets.go @@ -1,15 +1,14 @@ -package main +package models import ( - "fmt" - "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" -) -const targetTemplate = "//%s/%s:%s" + "github.com/divnix/std/cells/std/cli/data" + "github.com/divnix/std/cells/std/cli/keys" +) type TargetModel struct { List list.Model @@ -20,7 +19,7 @@ type TargetModel struct { func (m *TargetModel) Init() tea.Cmd { return nil } func (m *TargetModel) Update(msg tea.Msg) (*TargetModel, tea.Cmd) { var ( - appKeys = NewAppKeyMap() + appKeys = keys.NewAppKeyMap() cmd tea.Cmd ) switch msg := msg.(type) { @@ -46,7 +45,7 @@ func InitialTarget() *TargetModel { targetList := list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) targetList.Title = "Target" - targetList.KeyMap = DefaultListKeyMap() + targetList.KeyMap = keys.DefaultListKeyMap() targetList.SetFilteringEnabled(true) targetList.StartSpinner() targetList.DisableQuitKeybindings() @@ -55,15 +54,15 @@ func InitialTarget() *TargetModel { return &TargetModel{List: targetList} } -func (m *TargetModel) SelectedItem() *item { +func (m *TargetModel) SelectedItem() *data.Item { if m.List.SelectedItem() == nil { return nil } - var i = m.List.SelectedItem().(item) + var i = m.List.SelectedItem().(data.Item) return &i } -func (m *TargetModel) SetItems(l []item) { +func (m *TargetModel) SetItems(l []data.Item) { var numItems = cap(l) // Make list of actions items := make([]list.Item, numItems) @@ -88,29 +87,3 @@ func (m *TargetModel) FullHelp() [][]key.Binding { kb := [][]key.Binding{{}} return kb } - -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"` - StdReadme string `json:"__std_readme"` - actions []action -} - -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 = cap(i.actions) - // Make list of actions - items := make([]list.Item, numItems) - for j := 0; j < numItems; j++ { - items[j] = i.actions[j] - } - return items -} diff --git a/cells/std/cli/styles/styles.go b/cells/std/cli/styles/styles.go new file mode 100644 index 00000000..a0713570 --- /dev/null +++ b/cells/std/cli/styles/styles.go @@ -0,0 +1,28 @@ +package styles + +import ( + "github.com/charmbracelet/lipgloss" +) + +var ( + Highlight = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"} + AppStyle = lipgloss.NewStyle().Padding(1, 2) + + TargetStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(Highlight) + + 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..3360dba0 --- /dev/null +++ b/cells/std/cli/tui.go @@ -0,0 +1,275 @@ +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 + Spinner spinner.Model + Loading bool + 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 + ) + // 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 errMsg: + return m, tea.Quit + + 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 + } + // 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.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 { + m.Title = fmt.Sprintf(cmdTemplate, m.Target.SelectedItem().Title(), m.Action.SelectedItem().Title()) + } + return m, tea.Batch(cmds...) +} + +func (m *Tui) View() string { + var title string + if m.Loading { + title = styles.TitleStyle.Inline(true).Render("Loading") + " " + styles.TitleStyle.Inline(true).Render(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)), + )), + ) + } + + 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.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/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 +} From 971884dc95c87781dbcd5b12d161c5facc998639 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 18 Apr 2022 16:42:53 -0500 Subject: [PATCH 15/27] add nice debugging info when loading of std meta json fails --- cells/std/cli/flake.go | 10 +++++++++- cells/std/cli/go.mod | 2 ++ cells/std/cli/go.sum | 8 ++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/cells/std/cli/flake.go b/cells/std/cli/flake.go index bf94aff4..3dafbb96 100644 --- a/cells/std/cli/flake.go +++ b/cells/std/cli/flake.go @@ -6,6 +6,7 @@ import ( "log" "os/exec" + "github.com/TylerBrock/colorjson" tea "github.com/charmbracelet/bubbletea" "github.com/divnix/std/cells/std/cli/data" @@ -50,7 +51,14 @@ func loadFlake() tea.Msg { log.Fatal(err) } - json.Unmarshal(flakeStdMeta, &items) + if err := json.Unmarshal(flakeStdMeta, &items); err != nil { + var obj map[string]interface{} + json.Unmarshal(flakeStdMeta, &obj) + f := colorjson.NewFormatter() + f.Indent = 2 + s, _ := f.Marshal(obj) + log.Fatalf("%s - object: %s", err, s) + } return flakeLoadedMsg{ Items: fakeData(), diff --git a/cells/std/cli/go.mod b/cells/std/cli/go.mod index 2b3b0d98..9c4d0e23 100644 --- a/cells/std/cli/go.mod +++ b/cells/std/cli/go.mod @@ -3,8 +3,10 @@ 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.0 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 index c4f8f1ec..6145bc79 100644 --- a/cells/std/cli/go.sum +++ b/cells/std/cli/go.sum @@ -1,3 +1,5 @@ +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= @@ -23,6 +25,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs 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= @@ -32,6 +36,9 @@ github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+ 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= @@ -77,6 +84,7 @@ golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qx 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= From 8432498c82e9b12aaa3bd6d31d58a6f22c58cefd Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 18 Apr 2022 17:33:05 -0500 Subject: [PATCH 16/27] add first actual std meta parsing --- cells/std/cli/data/data.go | 18 +++++++++-------- cells/std/cli/flake.go | 13 +++++++++--- cells/std/cli/models/readme.go | 18 ++++++++--------- cells/std/cli/models/targets.go | 2 +- cells/std/cli/tui.go | 6 +++++- flake.nix | 35 ++++++++++++++++++++++----------- 6 files changed, 57 insertions(+), 35 deletions(-) diff --git a/cells/std/cli/data/data.go b/cells/std/cli/data/data.go index 0e5c6fac..2443e996 100644 --- a/cells/std/cli/data/data.go +++ b/cells/std/cli/data/data.go @@ -9,13 +9,15 @@ import ( 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"` - StdReadme string `json:"__std_readme"` - StdCladeActions []Action + 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 } func (i Item) Title() string { @@ -25,7 +27,7 @@ func (i Item) Description() string { return i.StdDescription } func (i Item) FilterValue() string { return i.Title() } func (i Item) GetActionItems() []list.Item { - var numItems = cap(i.StdCladeActions) + var numItems = len(i.StdCladeActions) // Make list of actions items := make([]list.Item, numItems) for j := 0; j < numItems; j++ { diff --git a/cells/std/cli/flake.go b/cells/std/cli/flake.go index 3dafbb96..c3bb8c56 100644 --- a/cells/std/cli/flake.go +++ b/cells/std/cli/flake.go @@ -52,7 +52,7 @@ func loadFlake() tea.Msg { } if err := json.Unmarshal(flakeStdMeta, &items); err != nil { - var obj map[string]interface{} + var obj interface{} json.Unmarshal(flakeStdMeta, &obj) f := colorjson.NewFormatter() f.Indent = 2 @@ -60,9 +60,16 @@ func loadFlake() tea.Msg { 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, + // Items: fakeData(), + Items: items, } } diff --git a/cells/std/cli/models/readme.go b/cells/std/cli/models/readme.go index 89c65e0f..3a6979a2 100644 --- a/cells/std/cli/models/readme.go +++ b/cells/std/cli/models/readme.go @@ -22,13 +22,13 @@ To create one, simply drop a file in: ${cellsFrom}/%s/%s/%s.md ` - noCellReadme = `Cell '%s' has no readme yet. + 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. + noOrganelleReadme = `Organelle '//%s/%s' has no readme yet. To create one, simply drop a file in: @@ -104,8 +104,8 @@ type renderTargetMarkdownMsg struct { func (m *ReadmeModel) SetTarget(t *data.Item) { m.Target = t m.HasTargetHelp = t.StdReadme != "" - m.HasCellHelp = false - m.HasOrganelleHelp = false + m.HasCellHelp = t.StdCellReadme != "" + m.HasOrganelleHelp = t.StdOrganelleReadme != "" if m.HasTargetHelp { m.TargetHelp.Viewport.SetContent(fmt.Sprintf("Rendering %s ...", t.StdReadme)) } else { @@ -116,7 +116,7 @@ func (m *ReadmeModel) SetTarget(t *data.Item) { m.TargetHelp.Viewport.SetContent(content) } if m.HasCellHelp { - m.CellHelp.Viewport.SetContent(fmt.Sprintf("Rendering %s ...", "TODO-CELL")) + m.CellHelp.Viewport.SetContent(fmt.Sprintf("Rendering %s ...", t.StdCellReadme)) } else { content := lipgloss.NewStyle(). Width(m.Width). @@ -125,7 +125,7 @@ func (m *ReadmeModel) SetTarget(t *data.Item) { m.CellHelp.Viewport.SetContent(content) } if m.HasOrganelleHelp { - m.OrganelleHelp.Viewport.SetContent(fmt.Sprintf("Rendering %s ...", "TODO-ORGANELLE")) + m.OrganelleHelp.Viewport.SetContent(fmt.Sprintf("Rendering %s ...", t.StdOrganelleReadme)) } else { content := lipgloss.NewStyle(). Width(m.Width). @@ -164,15 +164,13 @@ func (m *ReadmeModel) RenderMarkdown() tea.Cmd { m.TargetHelp.SetIsActive(true) if m.HasCellHelp { cmd = func() tea.Msg { - // TODO: get actual cell-readme - return renderCellMarkdownMsg{m.CellHelp.SetFileName(m.Target.StdReadme)()} + return renderCellMarkdownMsg{m.CellHelp.SetFileName(m.Target.StdCellReadme)()} } cmds = append(cmds, cmd) } if m.HasOrganelleHelp { cmd = func() tea.Msg { - // TODO: get actual organelle-readme - return renderOrganelleMarkdownMsg{m.OrganelleHelp.SetFileName(m.Target.StdReadme)()} + return renderOrganelleMarkdownMsg{m.OrganelleHelp.SetFileName(m.Target.StdOrganelleReadme)()} } cmds = append(cmds, cmd) } diff --git a/cells/std/cli/models/targets.go b/cells/std/cli/models/targets.go index 919549b8..d280d642 100644 --- a/cells/std/cli/models/targets.go +++ b/cells/std/cli/models/targets.go @@ -63,7 +63,7 @@ func (m *TargetModel) SelectedItem() *data.Item { } func (m *TargetModel) SetItems(l []data.Item) { - var numItems = cap(l) + var numItems = len(l) // Make list of actions items := make([]list.Item, numItems) for j := 0; j < numItems; j++ { diff --git a/cells/std/cli/tui.go b/cells/std/cli/tui.go index 3360dba0..c50e6853 100644 --- a/cells/std/cli/tui.go +++ b/cells/std/cli/tui.go @@ -186,7 +186,11 @@ func (m *Tui) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // As soon as targets are loaded, change the title if m.Target.SelectedItem() != nil { - m.Title = fmt.Sprintf(cmdTemplate, m.Target.SelectedItem().Title(), m.Action.SelectedItem().Title()) + if m.Action.SelectedItem() != nil { + m.Title = fmt.Sprintf(cmdTemplate, m.Target.SelectedItem().Title(), m.Action.SelectedItem().Title()) + } else { + m.Title = fmt.Sprintf(cmdTemplate, m.Target.SelectedItem().Title(), "NO ACTION") + } } return m, tea.Batch(cmds...) } diff --git a/flake.nix b/flake.nix index 3f98174e..bfd41340 100644 --- a/flake.nix +++ b/flake.nix @@ -100,9 +100,15 @@ 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 = {__std.${system} = 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 = { @@ -157,20 +163,25 @@ 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; + extractStdMeta = name: output: { + name = "${cellName}-${organelleName}-${name}"; + value = { + __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; + __std_readme = "./dummy_data/random-readme-1.md"; + __std_cell_readme = "./dummy_data/random-readme-2.md"; + __std_organelle_readme = ""; + }; }; 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; From 67cb39a968058c720e1c41175b3658ee8083c04f Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 18 Apr 2022 17:36:25 -0500 Subject: [PATCH 17/27] faint styling when action is missing --- cells/std/cli/tui.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cells/std/cli/tui.go b/cells/std/cli/tui.go index c50e6853..03874221 100644 --- a/cells/std/cli/tui.go +++ b/cells/std/cli/tui.go @@ -189,7 +189,7 @@ func (m *Tui) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.Action.SelectedItem() != nil { m.Title = fmt.Sprintf(cmdTemplate, m.Target.SelectedItem().Title(), m.Action.SelectedItem().Title()) } else { - m.Title = fmt.Sprintf(cmdTemplate, m.Target.SelectedItem().Title(), "NO ACTION") + m.Title = lipgloss.NewStyle().Faint(true).Render(fmt.Sprintf(cmdTemplate, m.Target.SelectedItem().Title(), "n/a")) } } return m, tea.Batch(cmds...) @@ -198,7 +198,7 @@ func (m *Tui) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *Tui) View() string { var title string if m.Loading { - title = styles.TitleStyle.Inline(true).Render("Loading") + " " + styles.TitleStyle.Inline(true).Render(m.Spinner.View()) + title = styles.TitleStyle.Render("Loading " + m.Spinner.View()) } else { title = styles.TitleStyle.Render(m.Title) } From 9ba15e0da43df8b33be2471d123e80c4b1ddee4b Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 18 Apr 2022 17:41:52 -0500 Subject: [PATCH 18/27] parse first actions --- cells/std/cli/data/data.go | 18 +++++++++--------- flake.nix | 7 +++++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/cells/std/cli/data/data.go b/cells/std/cli/data/data.go index 2443e996..d4ed288f 100644 --- a/cells/std/cli/data/data.go +++ b/cells/std/cli/data/data.go @@ -9,15 +9,15 @@ import ( 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 + 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 { diff --git a/flake.nix b/flake.nix index bfd41340..d8612dfe 100644 --- a/flake.nix +++ b/flake.nix @@ -176,6 +176,13 @@ __std_readme = "./dummy_data/random-readme-1.md"; __std_cell_readme = "./dummy_data/random-readme-2.md"; __std_organelle_readme = ""; + __std_actions = [ + { + __action_name = "run"; + __action_command = ["cowsay" "hi"]; + __action_description = "run this"; + } + ]; }; }; in { From 555794907340b496f31bba65edbdd554955595d1 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 18 Apr 2022 19:16:51 -0500 Subject: [PATCH 19/27] WIP --- cells/std/Readme.md | 1 + cells/std/{cli/default.nix => cli.nix} | 2 +- cells/std/cli/Readme.md | 1 + cells/std/cli/default.md | 1 + cells/std/devshellProfiles/Readme.md | 1 + cells/std/devshellProfiles/default.md | 1 + cells/std/lib/Readme.md | 1 + cells/std/lib/fromMakesWith.md | 1 + clades.nix | 18 ++++++ flake.nix | 85 ++++++++++++-------------- paths.nix | 15 +++++ validators.nix | 7 ++- 12 files changed, 85 insertions(+), 49 deletions(-) create mode 100644 cells/std/Readme.md rename cells/std/{cli/default.nix => cli.nix} (97%) create mode 100644 cells/std/cli/Readme.md create mode 100644 cells/std/cli/default.md create mode 100644 cells/std/devshellProfiles/Readme.md create mode 100644 cells/std/devshellProfiles/default.md create mode 100644 cells/std/lib/Readme.md create mode 100644 cells/std/lib/fromMakesWith.md create mode 100644 clades.nix create mode 100644 paths.nix 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/default.nix b/cells/std/cli.nix similarity index 97% rename from cells/std/cli/default.nix rename to cells/std/cli.nix index 47801402..8ca99145 100644 --- a/cells/std/cli/default.nix +++ b/cells/std/cli.nix @@ -16,7 +16,7 @@ in { pname = "std"; meta.description = "A tui for projects that conform to Standard"; - src = ./.; + src = ./cli; vendorSha256 = null; 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/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/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..6c063218 --- /dev/null +++ b/clades.nix @@ -0,0 +1,18 @@ +{ + runnables = name: { + inherit name; + clade = "runnables"; + }; + installables = name: { + inherit name; + clade = "installables"; + }; + functions = name: { + inherit name; + clade = "functions"; + }; + data = name: { + inherit name; + clade = "data"; + }; +} diff --git a/flake.nix b/flake.nix index d8612dfe..cf355efb 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; 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, @@ -104,9 +83,10 @@ # List of all flake outputs injected by std in the outputs and inputs.cells format stdOutputsFor = system: let acc = accumulate (builtins.map (loadCell system) Cells); + # flatten meta for easier ingestion by the std cli meta = {__std.${system} = builtins.attrValues acc.__std.${system};}; in - nixpkgs.lib.traceSeqN 4 meta + # nixpkgs.lib.traceSeqN 4 meta acc // meta; # Load a cell, return the flake outputs injected by std @@ -146,16 +126,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; @@ -163,19 +143,31 @@ nixpkgs.lib.attrsets.mapAttrsToList ( organelleName: output: let organelle = builtins.head organelles'.${organelleName}; - extractStdMeta = name: output: { + 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 = - output.meta.mainProgram or output.pname or output.name or name; + __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 = "./dummy_data/random-readme-1.md"; - __std_cell_readme = "./dummy_data/random-readme-2.md"; - __std_organelle_readme = ""; + __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 = [ { __action_name = "run"; @@ -272,10 +264,13 @@ in { inherit + (clades) runnables installables functions data + ; + inherit grow growOn harvest @@ -290,9 +285,9 @@ # as-nix-cli-epiphyte = false; cellsFrom = ./cells; organelles = [ - (runnables "cli") - (functions "lib") - (functions "devshellProfiles") + (clades.runnables "cli") + (clades.functions "lib") + (clades.functions "devshellProfiles") ]; 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/validators.nix b/validators.nix index 5a451353..2fa17533 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 @@ -71,7 +72,7 @@ in { clade = enum "clades" ["runnables" "installables" "functions" "data"]; } ); - MigrationNecesary = file: let + FileSignature = file: let file' = prefixWithCellsFrom file; in with yants "std" "import" file'; From eef0f5b9a981bc8f89f748ec7c02170f55af5489 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 18 Apr 2022 19:50:31 -0500 Subject: [PATCH 20/27] add some clade actions --- cells/std/cli/data/data.go | 6 +-- cells/std/data.nix | 6 +++ clades.nix | 71 ++++++++++++++++++++++++++++++- clades/data-write-action-expr.nix | 15 +++++++ flake.nix | 18 ++++---- validators.nix | 12 +++++- 6 files changed, 115 insertions(+), 13 deletions(-) create mode 100644 cells/std/data.nix create mode 100644 clades/data-write-action-expr.nix diff --git a/cells/std/cli/data/data.go b/cells/std/cli/data/data.go index d4ed288f..a7f7433b 100644 --- a/cells/std/cli/data/data.go +++ b/cells/std/cli/data/data.go @@ -37,9 +37,9 @@ func (i Item) GetActionItems() []list.Item { } type Action struct { - ActionName string `json:"__action_name"` - ActionCommand []string `json:"__action_command"` - ActionDescription string `json:"__action_description"` + ActionName string `json:"name"` + ActionCommand []string `json:"command"` + ActionDescription string `json:"description"` } func (a Action) Title() string { return a.ActionName } diff --git a/cells/std/data.nix b/cells/std/data.nix new file mode 100644 index 00000000..1dc34058 --- /dev/null +++ b/cells/std/data.nix @@ -0,0 +1,6 @@ +{ + inputs, + cell, +}: { + default = {}; +} diff --git a/clades.nix b/clades.nix index 6c063218..bf63ad20 100644 --- a/clades.nix +++ b/clades.nix @@ -1,11 +1,41 @@ -{ +{nixpkgs}: { runnables = name: { inherit name; clade = "runnables"; + actions = { + flake, + fragment, + }: [ + { + name = "run"; + description = "exec this target"; + command = ["nix" "run" "${flake}#${fragment}"]; + } + ]; }; installables = name: { inherit name; clade = "installables"; + actions = { + 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; @@ -14,5 +44,44 @@ data = name: { inherit name; clade = "data"; + actions = { + flake, + fragment, + }: [ + { + name = "write"; + description = "write to file"; + command = [ + "nix" + "build" + "--impure" + "--json" + "--no-link" + "--expr" + (builtins.readFile ./clades/data-write-action-expr.nix) + "|" + "jq" + "-r" + "'.[].outputs.out'" + ]; + } + { + name = "explore"; + description = "interactively explore (requires: 'fx')"; + command = [ + "nix" + "build" + "--impure" + "--expr" + (builtins.readFile ./clades/data-write-action-expr.nix) + "|" + "jq" + "-r" + "'.[].outputs.out'" + "|" + "fx" + ]; + } + ]; }; } diff --git a/clades/data-write-action-expr.nix b/clades/data-write-action-expr.nix new file mode 100644 index 00000000..ac1e292d --- /dev/null +++ b/clades/data-write-action-expr.nix @@ -0,0 +1,15 @@ +let + pkgs = + (builtins.getFlake "${nixpkgs.sourceInfo.outPath}") + .legacyPackages + .${builtins.currentSystem}; + this = + (builtins.getFlake "${flake.sourceInfo.outPath}") + ."${fragment}"; +in + pkgs.writeTextFile { + name = "data-clade-write"; + text = builtins.toJSON this; + executable = false; + destination = "/data"; + } diff --git a/flake.nix b/flake.nix index cf355efb..423ad1ad 100644 --- a/flake.nix +++ b/flake.nix @@ -12,7 +12,7 @@ nixpkgs = inputs'.nixpkgs; validate = import ./validators.nix {inherit (inputs') yants nixpkgs;}; paths = import ./paths.nix; - clades = import ./clades.nix; + clades = import ./clades.nix {inherit nixpkgs;}; incl = import ./incl.nix {inherit nixpkgs;}; deSystemize = system: s: if builtins.isAttrs s && builtins.hasAttr "${system}" s @@ -168,13 +168,14 @@ if builtins.pathExists oPath.readme then oPath.readme else ""; - __std_actions = [ - { - __action_name = "run"; - __action_command = ["cowsay" "hi"]; - __action_description = "run this"; - } - ]; + __std_actions = + if organelle ? actions + then + organelle.actions { + flake = inputs.self.sourceInfo.outPath; + fragment = ''"${system}"."${cellName}"."${organelleName}"."${name}"''; + } + else []; }; }; in { @@ -288,6 +289,7 @@ (clades.runnables "cli") (clades.functions "lib") (clades.functions "devshellProfiles") + (clades.data "data") ]; systems = ["x86_64-linux"]; } diff --git a/validators.nix b/validators.nix index 2fa17533..d0c9e8ce 100644 --- a/validators.nix +++ b/validators.nix @@ -65,11 +65,21 @@ 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 { + flake = false; + fragment = false; + }); } ); FileSignature = file: let From abec29d1e0a8c2b7d49531e7e6bdbdab540f8d70 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 18 Apr 2022 20:42:22 -0500 Subject: [PATCH 21/27] add proper error message display & example data --- cells/std/cli/flake.go | 31 ++++++++++-- cells/std/cli/styles/styles.go | 4 ++ cells/std/cli/tui.go | 18 ++++++- cells/std/data.nix | 2 +- cells/std/data/dummy-data.json | 91 ++++++++++++++++++++++++++++++++++ 5 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 cells/std/data/dummy-data.json diff --git a/cells/std/cli/flake.go b/cells/std/cli/flake.go index c3bb8c56..0f085a8e 100644 --- a/cells/std/cli/flake.go +++ b/cells/std/cli/flake.go @@ -8,6 +8,7 @@ import ( "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" @@ -39,6 +40,9 @@ func loadFlake() tea.Msg { // 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) } @@ -46,9 +50,18 @@ func loadFlake() tea.Msg { flakeStdMetaArgs = append(flakeStdMetaArgs, flakeStdMetaFragment) // load the std metadata from the flake - flakeStdMeta, err := exec.Command(nix, flakeStdMetaArgs...).Output() + cmd := exec.Command(nix, flakeStdMetaArgs...) + flakeStdMeta, err := cmd.Output() if err != nil { - log.Fatal(err) + 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 { @@ -77,6 +90,16 @@ type flakeLoadedMsg struct { Items []data.Item } -type errMsg struct{ err error } +type exitErrMsg struct { + cmd string + err *exec.ExitError +} -func (e errMsg) Error() string { return e.err.Error() } +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/styles/styles.go b/cells/std/cli/styles/styles.go index a0713570..25a658b2 100644 --- a/cells/std/cli/styles/styles.go +++ b/cells/std/cli/styles/styles.go @@ -8,6 +8,10 @@ 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) diff --git a/cells/std/cli/tui.go b/cells/std/cli/tui.go index 03874221..fcd8436a 100644 --- a/cells/std/cli/tui.go +++ b/cells/std/cli/tui.go @@ -43,6 +43,7 @@ type Tui struct { Title string Spinner spinner.Model Loading bool + Error string Focus Width int Height int @@ -71,8 +72,9 @@ func (m *Tui) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.Target.SetItems(msg.Items) return m, nil - case errMsg: - return m, tea.Quit + case exitErrMsg: + m.Error = msg.Error() + return m, nil case spinner.TickMsg: if m.Loading { @@ -214,6 +216,18 @@ func (m *Tui) View() string { ) } + 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)), + )), + ) + } + return lipgloss.Place(m.Width, m.Height, lipgloss.Center, lipgloss.Center, styles. AppStyle.MaxWidth(m.Width).MaxHeight(m.Height).Render( lipgloss.JoinVertical( diff --git a/cells/std/data.nix b/cells/std/data.nix index 1dc34058..0c3fbf13 100644 --- a/cells/std/data.nix +++ b/cells/std/data.nix @@ -2,5 +2,5 @@ inputs, cell, }: { - default = {}; + 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" + } +] From 659eb8b84cd0516d06f76389d62cc4613daca722 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 18 Apr 2022 23:08:05 -0500 Subject: [PATCH 22/27] make the data clade shine --- cells/std/cli/keys/keys.go | 26 ++++++++- cells/std/cli/models/actions.go | 90 ++++++++++++++++++++++++++++++- cells/std/cli/tui.go | 2 + clades.nix | 61 +++++++++++---------- clades/data-write-action-expr.nix | 15 ------ flake.nix | 1 + validators.nix | 1 + 7 files changed, 150 insertions(+), 46 deletions(-) delete mode 100644 clades/data-write-action-expr.nix diff --git a/cells/std/cli/keys/keys.go b/cells/std/cli/keys/keys.go index b93981ef..e4122309 100644 --- a/cells/std/cli/keys/keys.go +++ b/cells/std/cli/keys/keys.go @@ -17,9 +17,10 @@ var ( 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("?", "readme")) - closeReadme = key.NewBinding(key.WithKeys("?", "esc"), key.WithHelp("?", "close readme")) + 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")) @@ -97,6 +98,27 @@ func DefaultListKeyMap() list.KeyMap { } } +type ActionDelegateKeyMap struct { + Exec key.Binding + Inspect 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, + } +} + +func NewActionDelegateKeyMap() *ActionDelegateKeyMap { + return &ActionDelegateKeyMap{ + Exec: enter, + Inspect: showReadme, + } +} + // ViewportKeyMap returns a set of pager-like default keybindings. func ViewportKeyMap() viewport.KeyMap { return viewport.KeyMap{ diff --git a/cells/std/cli/models/actions.go b/cells/std/cli/models/actions.go index 36d76164..1378ab34 100644 --- a/cells/std/cli/models/actions.go +++ b/cells/std/cli/models/actions.go @@ -2,6 +2,11 @@ package models import ( "fmt" + "log" + "os" + "os/exec" + "strings" + "syscall" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" @@ -12,6 +17,87 @@ import ( "github.com/divnix/std/cells/std/cli/keys" ) +type DetachAndQuit int + +type ActionExitErrMsg struct { + cmd string + err *exec.ExitError +} + +func (e ActionExitErrMsg) 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), + ) +} + +func newActionDelegate(keys *keys.ActionDelegateKeyMap) list.DefaultDelegate { + d := list.NewDefaultDelegate() + + d.UpdateFunc = func(msg tea.Msg, m *list.Model) tea.Cmd { + var command []string + + if i, ok := m.SelectedItem().(data.Action); ok { + command = i.ActionCommand + } else { + return nil + } + + run := func() tea.Msg { + binary, lookErr := exec.LookPath("bash") + if lookErr != nil { + panic(lookErr) + } + args := []string{"bash", "-c", strings.Join(command, " ")} + env := os.Environ() + execErr := syscall.Exec(binary, args, env) + if execErr != nil { + panic(execErr) + } + cmd := exec.Command("bash", append([]string{"-c"}, command...)...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Start() + if err != nil { + switch exitErr := err.(type) { + case *exec.ExitError: + return ActionExitErrMsg{ + cmd: cmd.String(), + err: exitErr, + } + default: + log.Fatal(err) + } + } + // detach + cmd.Process.Release() + return DetachAndQuit(0) + } + + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, keys.Exec): + return run + + // case key.Matches(msg, keys.Inspect): + // return m.NewStatusMessage(statusMessageStyle("Deleted " + title)) + } + } + + return nil + } + + help := []key.Binding{keys.Exec, keys.Inspect} + 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 @@ -69,7 +155,9 @@ func (m *ActionModel) FullHelp() [][]key.Binding { func NewAction() *ActionModel { - actionList := list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) + 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) diff --git a/cells/std/cli/tui.go b/cells/std/cli/tui.go index fcd8436a..d869737b 100644 --- a/cells/std/cli/tui.go +++ b/cells/std/cli/tui.go @@ -67,6 +67,8 @@ func (m *Tui) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.Loading = false } switch msg := msg.(type) { + case models.DetachAndQuit: + return m, tea.Quit case flakeLoadedMsg: m.Target.SetItems(msg.Items) diff --git a/clades.nix b/clades.nix index bf63ad20..354d357a 100644 --- a/clades.nix +++ b/clades.nix @@ -3,6 +3,7 @@ inherit name; clade = "runnables"; actions = { + system, flake, fragment, }: [ @@ -17,6 +18,7 @@ inherit name; clade = "installables"; actions = { + system, flake, fragment, }: [ @@ -45,42 +47,45 @@ inherit name; clade = "data"; actions = { + system, flake, fragment, - }: [ + }: let + deps = [ + "nix" + "build" + "--no-link" + "${nixpkgs.sourceInfo.outPath}#fx" + ";" + "nix" + "build" + "--no-link" + "${nixpkgs.sourceInfo.outPath}#jq" + ";" + ]; + 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 = [ - "nix" - "build" - "--impure" - "--json" - "--no-link" - "--expr" - (builtins.readFile ./clades/data-write-action-expr.nix) - "|" - "jq" - "-r" - "'.[].outputs.out'" - ]; + command = deps ++ builder ++ jq; } { name = "explore"; - description = "interactively explore (requires: 'fx')"; - command = [ - "nix" - "build" - "--impure" - "--expr" - (builtins.readFile ./clades/data-write-action-expr.nix) - "|" - "jq" - "-r" - "'.[].outputs.out'" - "|" - "fx" - ]; + description = "interactively explore"; + command = deps ++ builder ++ jq ++ fx; } ]; }; diff --git a/clades/data-write-action-expr.nix b/clades/data-write-action-expr.nix deleted file mode 100644 index ac1e292d..00000000 --- a/clades/data-write-action-expr.nix +++ /dev/null @@ -1,15 +0,0 @@ -let - pkgs = - (builtins.getFlake "${nixpkgs.sourceInfo.outPath}") - .legacyPackages - .${builtins.currentSystem}; - this = - (builtins.getFlake "${flake.sourceInfo.outPath}") - ."${fragment}"; -in - pkgs.writeTextFile { - name = "data-clade-write"; - text = builtins.toJSON this; - executable = false; - destination = "/data"; - } diff --git a/flake.nix b/flake.nix index 423ad1ad..e1aef22c 100644 --- a/flake.nix +++ b/flake.nix @@ -172,6 +172,7 @@ if organelle ? actions then organelle.actions { + inherit system; flake = inputs.self.sourceInfo.outPath; fragment = ''"${system}"."${cellName}"."${organelleName}"."${name}"''; } diff --git a/validators.nix b/validators.nix index d0c9e8ce..64e2575d 100644 --- a/validators.nix +++ b/validators.nix @@ -77,6 +77,7 @@ in { name = string; clade = enum "clades" ["runnables" "installables" "functions" "data"]; actions = option (functionWithArgs { + system = false; flake = false; fragment = false; }); From 216f8c3bd1e761175ba33490be1d580f61cc91d6 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Tue, 19 Apr 2022 00:08:24 -0500 Subject: [PATCH 23/27] add action inspection --- cells/std/cli/keys/keys.go | 11 +++--- cells/std/cli/models/actions.go | 57 ++++++++--------------------- cells/std/cli/styles/styles.go | 4 +++ cells/std/cli/tui.go | 63 +++++++++++++++++++++++---------- 4 files changed, 69 insertions(+), 66 deletions(-) diff --git a/cells/std/cli/keys/keys.go b/cells/std/cli/keys/keys.go index e4122309..32ff5f7c 100644 --- a/cells/std/cli/keys/keys.go +++ b/cells/std/cli/keys/keys.go @@ -99,8 +99,9 @@ func DefaultListKeyMap() list.KeyMap { } type ActionDelegateKeyMap struct { - Exec key.Binding - Inspect key.Binding + Exec key.Binding + Inspect key.Binding + QuitInspect key.Binding } // Additional short help entries. This satisfies the help.KeyMap interface and @@ -109,13 +110,15 @@ func (d ActionDelegateKeyMap) ShortHelp() []key.Binding { return []key.Binding{ d.Exec, d.Inspect, + d.QuitInspect, } } func NewActionDelegateKeyMap() *ActionDelegateKeyMap { return &ActionDelegateKeyMap{ - Exec: enter, - Inspect: showReadme, + Exec: enter, + Inspect: showReadme, + QuitInspect: closeReadme, } } diff --git a/cells/std/cli/models/actions.go b/cells/std/cli/models/actions.go index 1378ab34..da467a8f 100644 --- a/cells/std/cli/models/actions.go +++ b/cells/std/cli/models/actions.go @@ -17,81 +17,52 @@ import ( "github.com/divnix/std/cells/std/cli/keys" ) -type DetachAndQuit int - -type ActionExitErrMsg struct { - cmd string - err *exec.ExitError -} - -func (e ActionExitErrMsg) 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), - ) -} +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 + 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 } - run := func() tea.Msg { + execve := func() tea.Msg { binary, lookErr := exec.LookPath("bash") if lookErr != nil { - panic(lookErr) + log.Fatal(lookErr) } - args := []string{"bash", "-c", strings.Join(command, " ")} env := os.Environ() execErr := syscall.Exec(binary, args, env) if execErr != nil { - panic(execErr) - } - cmd := exec.Command("bash", append([]string{"-c"}, command...)...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err := cmd.Start() - if err != nil { - switch exitErr := err.(type) { - case *exec.ExitError: - return ActionExitErrMsg{ - cmd: cmd.String(), - err: exitErr, - } - default: - log.Fatal(err) - } + log.Fatal(execErr) } - // detach - cmd.Process.Release() - return DetachAndQuit(0) + return nil } switch msg := msg.(type) { case tea.KeyMsg: switch { case key.Matches(msg, keys.Exec): - return run + return execve - // case key.Matches(msg, keys.Inspect): - // return m.NewStatusMessage(statusMessageStyle("Deleted " + title)) + case key.Matches(msg, keys.Inspect): + return func() tea.Msg { return ActionInspectMsg(strings.Join(args[2:], " ")) } } } return nil } - help := []key.Binding{keys.Exec, keys.Inspect} + help := []key.Binding{keys.Exec} d.ShortHelpFunc = func() []key.Binding { return help } d.FullHelpFunc = func() [][]key.Binding { return [][]key.Binding{} } diff --git a/cells/std/cli/styles/styles.go b/cells/std/cli/styles/styles.go index 25a658b2..8e3722e7 100644 --- a/cells/std/cli/styles/styles.go +++ b/cells/std/cli/styles/styles.go @@ -16,6 +16,10 @@ var ( BorderStyle(lipgloss.NormalBorder()). BorderForeground(Highlight) + ActionInspectionStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(Highlight) + ActionStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). BorderForeground(Highlight) diff --git a/cells/std/cli/tui.go b/cells/std/cli/tui.go index d869737b..6a95c0ec 100644 --- a/cells/std/cli/tui.go +++ b/cells/std/cli/tui.go @@ -35,15 +35,16 @@ func (s Focus) String() string { } type Tui struct { - Target *models.TargetModel - Action *models.ActionModel - Readme *models.ReadmeModel - Keys *keys.AppKeyMap - Legend help.Model - Title string - Spinner spinner.Model - Loading bool - Error string + 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 @@ -59,17 +60,15 @@ func (m *Tui) Init() tea.Cmd { func (m *Tui) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var ( - cmds []tea.Cmd - cmd tea.Cmd + 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 models.DetachAndQuit: - return m, tea.Quit - case flakeLoadedMsg: m.Target.SetItems(msg.Items) return m, nil @@ -78,6 +77,10 @@ func (m *Tui) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 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) @@ -91,6 +94,11 @@ func (m *Tui) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 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 @@ -105,10 +113,12 @@ func (m *Tui) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch { // toggle the help case key.Matches(msg, m.Keys.ShowReadme): - if !m.Readme.Active { - m.Readme.Active = true - cmd = m.Readme.RenderMarkdown() - return m, cmd + 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): @@ -229,6 +239,21 @@ func (m *Tui) View() string { )), ) } + 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( @@ -237,7 +262,7 @@ func (m *Tui) View() string { title, lipgloss.JoinHorizontal( lipgloss.Left, - styles.TargetStyle.Render(m.Target.View()), + 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)), From 84310bdf1ee4de43a2a224cdcfe91da1916d835c Mon Sep 17 00:00:00 2001 From: David Arnold Date: Tue, 19 Apr 2022 09:55:44 -0500 Subject: [PATCH 24/27] WIP --- cells/std/cli/styles/styles.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cells/std/cli/styles/styles.go b/cells/std/cli/styles/styles.go index 8e3722e7..0d59ee24 100644 --- a/cells/std/cli/styles/styles.go +++ b/cells/std/cli/styles/styles.go @@ -18,7 +18,7 @@ var ( ActionInspectionStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). - BorderForeground(Highlight) + BorderForeground(Highlight).Padding(0, 1).Faint(true) ActionStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). From 95021396856e89a40048fb717e0871fe064f8f78 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Wed, 20 Apr 2022 13:53:38 -0500 Subject: [PATCH 25/27] bump bubbletea (with tea.Cmd) --- cells/std/cli/go.mod | 2 +- cells/std/cli/go.sum | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cells/std/cli/go.mod b/cells/std/cli/go.mod index 9c4d0e23..898bd4a2 100644 --- a/cells/std/cli/go.mod +++ b/cells/std/cli/go.mod @@ -5,7 +5,7 @@ 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.0 + 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 index 6145bc79..2809289a 100644 --- a/cells/std/cli/go.sum +++ b/cells/std/cli/go.sum @@ -11,6 +11,8 @@ github.com/charmbracelet/bubbles v0.10.3/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPAL 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= @@ -54,6 +56,8 @@ github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTd 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= @@ -93,6 +97,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc 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= From 6bdd96a922f556bc98443ee78464786e1cc92914 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Wed, 20 Apr 2022 14:35:15 -0500 Subject: [PATCH 26/27] use derivation to slightly speed up (and prep for nix-eval-cache) --- cells/std/cli/flake.go | 33 +++++++++++++++++++++++++++++++-- flake.nix | 10 ++++++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/cells/std/cli/flake.go b/cells/std/cli/flake.go index 0f085a8e..1dca0ad8 100644 --- a/cells/std/cli/flake.go +++ b/cells/std/cli/flake.go @@ -3,7 +3,9 @@ package main import ( "encoding/json" "fmt" + "io/ioutil" "log" + "os" "os/exec" "github.com/TylerBrock/colorjson" @@ -14,10 +16,17 @@ import ( "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{"eval", "--json", "--option", "warn-dirty", "false"} + flakeStdMetaArgs = []string{"build", "--no-link", "--json", "--option", "warn-dirty", "false"} + flakeStdBuildOut = []map[string]interface{}{} ) func fakeData() []data.Item { @@ -51,7 +60,27 @@ func loadFlake() tea.Msg { // load the std metadata from the flake cmd := exec.Command(nix, flakeStdMetaArgs...) - flakeStdMeta, err := cmd.Output() + 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: diff --git a/flake.nix b/flake.nix index e1aef22c..bbd0b7a1 100644 --- a/flake.nix +++ b/flake.nix @@ -83,8 +83,14 @@ # List of all flake outputs injected by std in the outputs and inputs.cells format stdOutputsFor = system: let acc = accumulate (builtins.map (loadCell system) Cells); - # flatten meta for easier ingestion by the std cli - meta = {__std.${system} = builtins.attrValues acc.__std.${system};}; + 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 From bc2458f1d3c1ff6c70bd06c35657782ff213e50f Mon Sep 17 00:00:00 2001 From: David Arnold Date: Wed, 20 Apr 2022 17:50:20 -0500 Subject: [PATCH 27/27] avoid necessity to declare dependencies --- clades.nix | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/clades.nix b/clades.nix index 354d357a..bdaf0e98 100644 --- a/clades.nix +++ b/clades.nix @@ -51,18 +51,6 @@ flake, fragment, }: let - deps = [ - "nix" - "build" - "--no-link" - "${nixpkgs.sourceInfo.outPath}#fx" - ";" - "nix" - "build" - "--no-link" - "${nixpkgs.sourceInfo.outPath}#jq" - ";" - ]; 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"]; @@ -80,12 +68,12 @@ { name = "write"; description = "write to file"; - command = deps ++ builder ++ jq; + command = builder ++ jq; } { name = "explore"; description = "interactively explore"; - command = deps ++ builder ++ jq ++ fx; + command = builder ++ jq ++ fx; } ]; };