├── .envrc
├── .github
├── settings.yml
└── workflows
│ └── gh-pages.yaml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── VERSION
├── book.toml
├── docs
├── SUMMARY.md
├── audience.md
├── flake.nix
├── introduction.md
├── motivation.md
├── rebranding-nix.md
└── tui-reference.md
├── flake.lock
├── flake.nix
├── mdbook-paisano-preprocessor.css
├── nix
├── repo
│ ├── config.nix
│ └── shells.nix
└── tui
│ ├── app.md
│ └── app.nix
└── src
├── .gitignore
├── cache
├── cache.go
├── cache_test.go
└── hash.go
├── cli.go
├── data
└── data.go
├── default.md
├── env
└── env.go
├── flake
├── flake.go
├── load.go
└── run.go
├── go.mod
├── go.sum
├── keys
└── keys.go
├── load.go
├── main.go
├── models
└── readme.go
├── styles
└── styles.go
└── tui.go
/.envrc:
--------------------------------------------------------------------------------
1 | #! /bin/sh
2 |
3 | # shellcheck disable=SC1090
4 | . "$(fetchurl "https://raw.githubusercontent.com/paisano-nix/direnv/main/lib" "sha256-IgQhKK7UHL1AfCUntJO2KCaIDJQotRnK2qC4Daxk+wI=")"
5 |
6 | use envreload //repo/shells/default //repo/config
7 |
--------------------------------------------------------------------------------
/.github/settings.yml:
--------------------------------------------------------------------------------
1 | labels:
2 | - color: '#000000'
3 | description: This issue has been abdandoned
4 | name: ':running: Status: Abdandoned'
5 | - color: '#4CAF50'
6 | description: This issue has been accepted
7 | name: ':ok: Status: Accepted'
8 | - color: '#F44336'
9 | description: This issue is in a blocking state
10 | name: ':x: Status: Blocked'
11 | - color: '#A6A6A6'
12 | description: This issue is actively being worked on
13 | name: ':construction: Status: In Progress'
14 | - color: '#F44336'
15 | description: This issue is not currently being worked on
16 | name: ':golf: Status: On Hold'
17 | - color: '#FDD835'
18 | description: This issue is pending a review
19 | name: ':eyes: Status: Review Needed'
20 | - color: '#F44336'
21 | description: This issue targets a bug
22 | name: ':bug: Type: Bug'
23 | - color: '#FB8C00'
24 | description: This issue targets general maintenance
25 | name: ':wrench: Type: Maintenance'
26 | - color: '#AB47BC'
27 | description: This issue contains a question
28 | name: ':grey_question: Type: Question'
29 | - color: '#F44336'
30 | description: This issue targets a security vulnerability
31 | name: ':cop: Type: Security'
32 | - color: '#64B5F6'
33 | description: This issue targets a new feature through a story
34 | name: ':scroll: Type: Story'
35 | - color: '#F44336'
36 | description: This issue is prioritized as critical
37 | name: ':boom: Priority: Critical'
38 | - color: '#FB8C00'
39 | description: This issue is prioritized as high
40 | name: ':fire: Priority: High'
41 | - color: '#4CAF50'
42 | description: This issue is prioritized as low
43 | name: ':low_brightness: Priority: Low'
44 | - color: '#FFEE58'
45 | description: This issue is prioritized as medium
46 | name: ':star2: Priority: Medium'
47 | - color: '#4CAF50'
48 | description: This issue is of low complexity or very well understood
49 | name: ':muscle: Effort: 1'
50 | - color: '#FFEE58'
51 | description: This issue is of medium complexity or only partly well understood
52 | name: ':muscle: Effort: 3'
53 | - color: '#F44336'
54 | description: This issue is of high complexity or just not yet well understood
55 | name: ':muscle: Effort: 5'
56 | repository:
57 | allow_merge_commit: false
58 | allow_rebase_merge: true
59 | allow_squash_merge: true
60 | default_branch: main
61 | delete_branch_on_merge: true
62 | description: Paisano's TUI/CLI companion
63 | has_downloads: false
64 | has_issues: true
65 | has_projects: false
66 | has_wiki: false
67 | homepage: https://paisano-nix.github.io/tui
68 | name: tui
69 | private: false
70 | topics: nix, nix-flakes, flake, ux, tui, cli
71 |
--------------------------------------------------------------------------------
/.github/workflows/gh-pages.yaml:
--------------------------------------------------------------------------------
1 | name: github pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | # Build job
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 |
15 | - uses: nixbuild/nix-quick-install-action@v26
16 | with:
17 | nix_conf: |
18 | experimental-features = nix-command flakes
19 | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
20 |
21 | - run: nix develop --no-write-lock-file ./docs#mdbook -c mdbook build
22 |
23 | - name: Upload GitHub Pages artifact
24 | uses: actions/upload-pages-artifact@v1.0.7
25 | with:
26 | # Path of the directory containing the static assets.
27 | path: docs/build/html
28 | # Duration after which artifact will expire in days.
29 | retention-days: # optional, default is 1
30 |
31 | # Deploy job
32 | deploy:
33 | # Add a dependency to the build job
34 | needs: build
35 |
36 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment
37 | permissions:
38 | pages: write # to deploy to Pages
39 | id-token: write # to verify the deployment originates from an appropriate source
40 |
41 | # Deploy to the github-pages environment
42 | environment:
43 | name: github-pages
44 | url: ${{ steps.deployment.outputs.page_url }}
45 |
46 | # Specify runner + deployment step
47 | runs-on: ubuntu-latest
48 | steps:
49 | - name: Deploy to GitHub Pages
50 | id: deployment
51 | uses: actions/deploy-pages@v1
52 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .std
2 | result
3 |
4 | # nixago: ignore-linked-files
5 | /cog.toml
6 | /lefthook.yml
7 | /.conform.yaml
8 | /treefmt.toml
9 | adrgen.config.yml
10 | lefthook.yml
11 | .editorconfig
12 | .conform.yaml
13 | treefmt.toml
14 |
15 | # nixago-auto-created: mdbook-build-folder
16 | docs/build
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.
3 |
4 | - - -
5 | ## [v0.5.0](https://github.com/paisano-nix/tui/compare/v0.4.3..v0.5.0) - 2024-02-22
6 | #### Build system
7 | - bump version of std tooling - ([01a2ab0](https://github.com/paisano-nix/tui/commit/01a2ab07b8fdeea407d16b376f469fdd297126d4)) - David Arnold
8 | #### Continuous Integration
9 | - update nix version in ci - ([6512f8c](https://github.com/paisano-nix/tui/commit/6512f8c755cbd20b7ac3a55e0126a0b5a783257b)) - David Arnold
10 | #### Features
11 | - improve cli/tui ouput - ([2fff01a](https://github.com/paisano-nix/tui/commit/2fff01a54bb4fae0daee3921b4360fdc695570c4)) - David Arnold
12 |
13 | - - -
14 |
15 | ## [v0.4.3](https://github.com/paisano-nix/tui/compare/v0.4.2..v0.4.3) - 2024-02-19
16 | #### Build system
17 | - bump inpuuts - ([7fe73ab](https://github.com/paisano-nix/tui/commit/7fe73abac6e2303548a6b1617ae6257d655a9b69)) - David Arnold
18 |
19 | - - -
20 |
21 | ## [v0.4.2](https://github.com/paisano-nix/tui/compare/v0.4.1..v0.4.2) - 2024-02-19
22 | #### Build system
23 | - fix with go mod tidy - ([d59f7a4](https://github.com/paisano-nix/tui/commit/d59f7a4d05e42216c90687d4beb17af39058b136)) - David Arnold
24 |
25 | - - -
26 |
27 | ## [v0.4.1](https://github.com/paisano-nix/tui/compare/v0.4.0..v0.4.1) - 2024-02-17
28 | #### Bug Fixes
29 | - std data for cog patch release flow - ([37f06fa](https://github.com/paisano-nix/tui/commit/37f06fa0846119ae2b15ffcb2eecb1462c24f894)) - David Arnold
30 | - vendor sha - ([8121467](https://github.com/paisano-nix/tui/commit/8121467cc676d9887671d7533c9687f53e92c241)) - David Arnold
31 | #### Miscellaneous Chores
32 | - update std to see if cog data has been fixed - ([8d1c341](https://github.com/paisano-nix/tui/commit/8d1c3412f49a1212846d70ff97b49929f357c64e)) - David Arnold
33 | - remove benchmark-like test for faster build time - ([ccfcaec](https://github.com/paisano-nix/tui/commit/ccfcaec93068eac196e4222ebe53b3599c7be071)) - pegasust
34 |
35 | - - -
36 |
37 | ## [v0.4.0](https://github.com/paisano-nix/tui/compare/v0.3.0..v0.4.0) - 2024-02-17
38 | #### Miscellaneous Chores
39 | - update prj-spec to latest version - ([d4007c2](https://github.com/paisano-nix/tui/commit/d4007c245a816c855976ff0ccf94233024ce64cd)) - David Arnold
40 | - fix cocogitto config - ([4cfe6c1](https://github.com/paisano-nix/tui/commit/4cfe6c1ceef85c413a31c9d4fdfd42679fe3e92b)) - David Arnold
41 |
42 | - - -
43 |
44 | ## [v0.3.0](https://github.com/divnix/hive/compare/v0.2.0..v0.3.0) - 2023-09-15
45 | #### Miscellaneous Chores
46 | - adopt local cell best practices - ([4874308](https://github.com/divnix/hive/commit/48743087ad1c61d9cb84551ace2955be54bf53d4)) - David Arnold
47 |
48 | - - -
49 |
50 | ## [0.2.0](https://github.com/paisano-nix/tui/compare/0.1.1..0.2.0) - 2023-09-06
51 | #### Bug Fixes
52 | - quote action invokations as they may contain '.' - ([c2f752b](https://github.com/paisano-nix/tui/commit/c2f752b4f288468c2190367afad1d66bc959d4bd)) - [@blaggacao](https://github.com/blaggacao)
53 | - show nix build instead of nix run - ([1dd6997](https://github.com/paisano-nix/tui/commit/1dd69975f3cec30a2f4ababde28d7cf8c1a61b3d)) - [@blaggacao](https://github.com/blaggacao)
54 |
55 | - - -
56 |
57 | ## [0.1.1](https://github.com/paisano-nix/tui/compare/0.1.0..0.1.1) - 2023-04-18
58 | #### Bug Fixes
59 | - pass args along to the final invocation - ([83af50d](https://github.com/paisano-nix/tui/commit/83af50d6c058999094bfab633e0a50faedafa1d1)) - [@blaggacao](https://github.com/blaggacao)
60 | - cog config - ([507cd13](https://github.com/paisano-nix/tui/commit/507cd138a26807aac2be5b859c9000aad8283203)) - [@blaggacao](https://github.com/blaggacao)
61 | #### Miscellaneous Chores
62 | - add instructions to publish release notesd - ([ad7ba7a](https://github.com/paisano-nix/tui/commit/ad7ba7a1cbc0302103a25d1262c9eb55c0939223)) - [@blaggacao](https://github.com/blaggacao)
63 |
64 | - - -
65 |
66 | ## [0.1.0](https://github.com/tui/paisano-nix/compare/5eef783baf77df737e33e8265834ac8afd0b78df..0.1.0) - 2023-04-17
67 | #### Bug Fixes
68 | - polish the completion ux and add some bling - ([3859076](https://github.com/tui/paisano-nix/commit/38590763cbbdf3175cf62b1c693f83c449313e54)) - [@blaggacao](https://github.com/blaggacao)
69 | - infinit loop on prj-spec init if outside a project repo - ([9b2bf76](https://github.com/tui/paisano-nix/commit/9b2bf7679b671319b96fc24f534620f1d9f27f0f)) - [@blaggacao](https://github.com/blaggacao)
70 | - branding on the `check` sub command - ([1c84e60](https://github.com/tui/paisano-nix/commit/1c84e604adb8907bc20ee5030bb124020ac79ace)) - [@blaggacao](https://github.com/blaggacao)
71 | - nil deref - damint cobra - ([f0272b3](https://github.com/tui/paisano-nix/commit/f0272b3986fbf153322b6e1c8b13016830e3577a)) - [@blaggacao](https://github.com/blaggacao)
72 | - bump std - ([802958d](https://github.com/tui/paisano-nix/commit/802958d123b0a5437441be0cab1dee487b0ed3eb)) - [@blaggacao](https://github.com/blaggacao)
73 | - oversight so that current system is detected again - ([bf8ef13](https://github.com/tui/paisano-nix/commit/bf8ef13f4ad9c84e7bf177c8a5f1c9586c41a4e4)) - [@blaggacao](https://github.com/blaggacao)
74 | #### Continuous Integration
75 | - add gh pages action - ([9756b9a](https://github.com/tui/paisano-nix/commit/9756b9aacc3ab369016c5b56677bf0e8902e8e01)) - [@blaggacao](https://github.com/blaggacao)
76 | #### Documentation
77 | - add tagline description - ([9f03a91](https://github.com/tui/paisano-nix/commit/9f03a911b9293acd93c3fbb1cf1cdaa92ec89c13)) - [@blaggacao](https://github.com/blaggacao)
78 | - add flake-view for docs with mdbook-paisano-preprocessor - ([f45d054](https://github.com/tui/paisano-nix/commit/f45d054b1329e70e475eb185367d18fa08a6a176)) - [@blaggacao](https://github.com/blaggacao)
79 | - fix intro page link - ([92488a2](https://github.com/tui/paisano-nix/commit/92488a29c7b9feac773feba8672d406d5268e3ae)) - [@blaggacao](https://github.com/blaggacao)
80 | - improve wording - ([0fe8858](https://github.com/tui/paisano-nix/commit/0fe88586963807b918cab3e4a6a651604b0a82c2)) - [@blaggacao](https://github.com/blaggacao)
81 | - add rebranding example - ([f32aaec](https://github.com/tui/paisano-nix/commit/f32aaec2774be698590c45438c2b8d0d5cbfa87e)) - [@blaggacao](https://github.com/blaggacao)
82 | - add documentation - ([6c52cf0](https://github.com/tui/paisano-nix/commit/6c52cf0de2e0acd88aef3515f909936abfebb4b6)) - [@blaggacao](https://github.com/blaggacao)
83 | #### Features
84 | - improve description on CLI completion - ([830d91f](https://github.com/tui/paisano-nix/commit/830d91ff32d3e12a4f89dec2f74179416af513c8)) - [@blaggacao](https://github.com/blaggacao)
85 | - comply with PRJ Spec (akin XDG_*) - ([de2574d](https://github.com/tui/paisano-nix/commit/de2574dc7390a9f38ace10b3cb3b35737595f365)) - [@blaggacao](https://github.com/blaggacao)
86 | - add license - ([cb9ac8b](https://github.com/tui/paisano-nix/commit/cb9ac8bc142c6bfac2bebb6566a03175aeb97a05)) - [@blaggacao](https://github.com/blaggacao)
87 | - actions on current system when (remote) build for other system - ([cd31e1c](https://github.com/tui/paisano-nix/commit/cd31e1c13aa01fa811d21b522215037c57e03cd3)) - [@blaggacao](https://github.com/blaggacao)
88 | #### Miscellaneous Chores
89 | - instrument release - ([40fab50](https://github.com/tui/paisano-nix/commit/40fab501a95f1a7f966f0b392557a01c1bcd2b60)) - [@blaggacao](https://github.com/blaggacao)
90 | - add hint to commit readme files - ([f080910](https://github.com/tui/paisano-nix/commit/f0809101b957e831ff5ae3be432397a0da9149b7)) - [@blaggacao](https://github.com/blaggacao)
91 | #### Refactoring
92 | - improve and clean up the code; optional `nom` support - ([2896332](https://github.com/tui/paisano-nix/commit/2896332e412153d7110bac3ebf330e9c5e34404b)) - [@blaggacao](https://github.com/blaggacao)
93 | - use new and shiny paisano direnv support - ([7db9c76](https://github.com/tui/paisano-nix/commit/7db9c76c3e440a926faf3efa585faf1d080585de)) - [@blaggacao](https://github.com/blaggacao)
94 | - make branding configurable at build time - ([694baa7](https://github.com/tui/paisano-nix/commit/694baa76fd58492b721f9091f2ed6736bfa6d85e)) - [@blaggacao](https://github.com/blaggacao)
95 |
96 | - - -
97 |
98 | Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto).
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
4 |
5 | In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and
6 | successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
7 |
8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 |
10 | For more information, please refer to
11 |
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # The `paisano` TUI / CLI
2 |
3 | The Paisano TUI / CLI is a brandable general purpose command line kit for Flake based projects which fulfill a Paisano layout and importer contract.
4 |
5 | ## Usage
6 |
7 | - Install Paisano: `nix profile install github:paisano-nix/tui`
8 | - Set up autocompletion (optional): `paisano _carapace [SHELL]` — see [carapace docs][carapace-docs]
9 | - Enter a Paisano-based repository.
10 | - Run `paisano` or `paisano list` and profit ✨!
11 |
12 | [carapace-docs]: https://rsteube.github.io/carapace/carapace/gen/hiddenSubcommand.html
13 |
14 | ## Branding
15 |
16 | To change the branding of this binary you can set these variables via `-X` compile flag:
17 |
18 | ```
19 | main.buildVersion | default: dev
20 | main.buildCommit | default: dirty
21 | main.argv0 | default: paisano
22 | main.project | default: Paisano
23 | flake.registry | default: __std # temp kept, mainly for `std-action`
24 | ```
25 |
26 | Example: `go build -o my-bin-name -ldflags="-X main.argv0=hive -X main.project=Hive"`
27 |
28 | ## Contributing
29 |
30 | ##### Prerequisites
31 |
32 | You need [nix](https://nixos.org/download.html) and [direnv](https://direnv.net/).
33 |
34 | ##### Enter Contribution Environment
35 |
36 | ```console
37 | direnv allow
38 | ```
39 |
40 | ##### Change Contribution Environment
41 |
42 | ```console
43 | $EDITOR ./nix/repo/config.nix
44 | direnv reload
45 | ```
46 |
47 | ##### Preview Documentation
48 |
49 | You need to be inside the Contribution Environment.
50 |
51 | ```console
52 | mdbook build -o
53 | ```
54 |
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 0.6.0-dev
2 |
--------------------------------------------------------------------------------
/book.toml:
--------------------------------------------------------------------------------
1 | [book]
2 | language = "en"
3 | multilingual = false
4 | src = "docs"
5 | title = "Paisano TUI Book"
6 |
7 | [build]
8 | build-dir = "docs/book"
9 |
10 | [output]
11 | [output.html]
12 | additional-css = ["./mdbook-paisano-preprocessor.css"]
13 |
14 | [output.linkcheck]
15 |
16 | [preprocessor.paisano-preprocessor]
17 | before = ["links"]
18 | registry = ".#__std.init"
19 |
20 | [[preprocessor.paisano-preprocessor.multi]]
21 | cell = "tui"
22 | chapter = "TUI Reference"
23 |
--------------------------------------------------------------------------------
/docs/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Prelude
2 |
3 | - [Introduction](introduction.md)
4 | - [Motivation](motivation.md)
5 | - [Target Audience](audience.md)
6 |
7 | # Reference
8 |
9 | - [TUI Reference](tui-reference.md)
10 | - [Rebranding with Nix](rebranding-nix.md)
11 |
--------------------------------------------------------------------------------
/docs/audience.md:
--------------------------------------------------------------------------------
1 | # Intended Audience
2 |
3 | ## End user
4 |
5 | End users of this tool can supercharge any Paisano-based project with discoverability.
6 |
7 | However, if a framework-specific branded version is available, that should be used instead.
8 |
9 | ## Framework creator
10 |
11 | This repository implements a white-label version of the Paisano repository companion.
12 |
13 | Downstream frameworks, such as [Standard][std] or [Hive][hive] can re-brand this tool to offer a seamless and branded experience to their users.
14 |
15 | [std]: https://github.com/divnix/std
16 | [hive]: https://github.com/divnix/hive
17 |
18 | Please refer to the readme on how to re-brand.
19 |
--------------------------------------------------------------------------------
/docs/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | # A flake-view into the top-level flake's contrib env
3 | # with `mdbook-paisano-preprocessor' available in inputs
4 | description = "Paisano's TUI/CLI extended docs env";
5 |
6 | inputs.super.url = "path:../.";
7 | inputs.mdbook-paisano-preprocessor.url = "github:paisano-nix/mdbook-paisano-preprocessor";
8 | inputs.std.follows = "super/std";
9 | inputs.nixpkgs.follows = "super/std/nixpkgs";
10 |
11 | outputs = {
12 | std,
13 | self,
14 | super,
15 | ...
16 | } @ inputs:
17 | std.growOn {
18 | inherit inputs;
19 | cellsFrom = std.incl ../nix [
20 | "repo"
21 | ];
22 | cellBlocks = with std.blockTypes; [
23 | # Development Environments
24 | (nixago "config")
25 | (devshells "shells")
26 | ];
27 | }
28 | {
29 | devShells = std.harvest self ["repo" "shells"];
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/docs/introduction.md:
--------------------------------------------------------------------------------
1 | ../README.md
--------------------------------------------------------------------------------
/docs/motivation.md:
--------------------------------------------------------------------------------
1 | # Motivation
2 |
3 | # Problem
4 |
5 | A Paisano-based repository has a well-defined folder structure.
6 |
7 | That folder structure is parsed and transformed by Paisano into flake outputs.
8 |
9 | However, as a project grows, and so does the number of outputs, it becomes increasingly hard to discover all of them easily.
10 |
11 | When it already becomes complicated to find stuff for Nix experts, it is even more difficult for a user who is not deeply familiar with Nix.
12 |
13 | Hence, as a project grows, we face unsolved problems of discoverability.
14 |
15 | # Solution
16 |
17 | We could, of course, write a readme and detail all outputs and bespoke ways of how to interact with them.
18 |
19 | But we all know a readme's flaws:
20 |
21 | - people tend to not read it, especially as it grows larger
22 | - it tends to become outdated, coincidentally also as it grows larger
23 |
24 | # Better Solution
25 |
26 | To solve this discoverability problem while never becoming outdated, this tool renders all Paisano-based flake outputs into an easily browsable and searchable terminal user interface. A variety of actions offer the user pre-defined and discoverable ways to interact with these outputs based on their types.
27 |
28 | Once the user knows what she's looking for, she can choose to access her intended target action directly by using this tool as a CLI.
29 |
--------------------------------------------------------------------------------
/docs/rebranding-nix.md:
--------------------------------------------------------------------------------
1 | # How To: Rebrand using Nix
2 |
3 | Example taken from [Standard][std].
4 |
5 | [std]: https://std.divnix.com
6 |
7 | ```nix
8 | let
9 | version = "0.15.0+dev";
10 |
11 | inherit (inputs) nixpkgs;
12 | inherit (nixpkgs.lib) licenses;
13 | in {
14 | default = cell.cli.std;
15 |
16 | std = nixpkgs.buildGoModule rec {
17 | inherit version;
18 | pname = "std";
19 | meta = {
20 | inherit (import (inputs.self + /flake.nix)) description;
21 | license = licenses.unlicense;
22 | homepage = "https://github.com/divnix/std";
23 | };
24 |
25 | src = inputs.paisano-tui.sourceInfo + /src;
26 |
27 | vendorHash = "sha256-1le14dcr2b8TDUNdhIFbZGX3khQoCcEZRH86eqlZaQE=";
28 |
29 | nativeBuildInputs = [nixpkgs.installShellFiles];
30 |
31 | postInstall = ''
32 | mv $out/bin/paisano $out/bin/${pname}
33 |
34 | installShellCompletion --cmd std \
35 | --bash <($out/bin/std _carapace bash) \
36 | --fish <($out/bin/std _carapace fish) \
37 | --zsh <($out/bin/std _carapace zsh)
38 | '';
39 |
40 | ldflags = [
41 | "-s"
42 | "-w"
43 | "-X main.buildVersion=${version}"
44 | "-X main.argv0=${pname}"
45 | "-X main.project=Standard"
46 | "-X flake.registry=__std"
47 | ];
48 | };
49 | }
50 |
51 | ```
52 |
--------------------------------------------------------------------------------
/docs/tui-reference.md:
--------------------------------------------------------------------------------
1 | # Reference
2 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "blank": {
4 | "locked": {
5 | "lastModified": 1625557891,
6 | "narHash": "sha256-O8/MWsPBGhhyPoPLHZAuoZiiHo9q6FLlEeIDEXuj6T4=",
7 | "owner": "divnix",
8 | "repo": "blank",
9 | "rev": "5a5d2684073d9f563072ed07c871d577a6c614a8",
10 | "type": "github"
11 | },
12 | "original": {
13 | "owner": "divnix",
14 | "repo": "blank",
15 | "type": "github"
16 | }
17 | },
18 | "call-flake": {
19 | "locked": {
20 | "lastModified": 1687380775,
21 | "narHash": "sha256-bmhE1TmrJG4ba93l9WQTLuYM53kwGQAjYHRvHOeuxWU=",
22 | "owner": "divnix",
23 | "repo": "call-flake",
24 | "rev": "74061f6c241227cd05e79b702db9a300a2e4131a",
25 | "type": "github"
26 | },
27 | "original": {
28 | "owner": "divnix",
29 | "repo": "call-flake",
30 | "type": "github"
31 | }
32 | },
33 | "devshell": {
34 | "inputs": {
35 | "nixpkgs": [
36 | "nixpkgs"
37 | ],
38 | "systems": "systems"
39 | },
40 | "locked": {
41 | "lastModified": 1694435990,
42 | "narHash": "sha256-yLQPD2eZGepu3yvdwABXrR3GhAqWRWTj9rn3a4knYuk=",
43 | "owner": "numtide",
44 | "repo": "devshell",
45 | "rev": "f6aec2e8b1cdddcab10ce7fc2eac66886e3deaad",
46 | "type": "github"
47 | },
48 | "original": {
49 | "owner": "numtide",
50 | "repo": "devshell",
51 | "type": "github"
52 | }
53 | },
54 | "dmerge": {
55 | "inputs": {
56 | "haumea": [
57 | "std",
58 | "haumea"
59 | ],
60 | "nixlib": [
61 | "std",
62 | "lib"
63 | ],
64 | "yants": [
65 | "std",
66 | "yants"
67 | ]
68 | },
69 | "locked": {
70 | "lastModified": 1686862774,
71 | "narHash": "sha256-ojGtRQ9pIOUrxsQEuEPerUkqIJEuod9hIflfNkY+9CE=",
72 | "owner": "divnix",
73 | "repo": "dmerge",
74 | "rev": "9f7f7a8349d33d7bd02e0f2b484b1f076e503a96",
75 | "type": "github"
76 | },
77 | "original": {
78 | "owner": "divnix",
79 | "ref": "0.2.1",
80 | "repo": "dmerge",
81 | "type": "github"
82 | }
83 | },
84 | "flake-utils": {
85 | "locked": {
86 | "lastModified": 1653893745,
87 | "narHash": "sha256-0jntwV3Z8//YwuOjzhV2sgJJPt+HY6KhU7VZUL0fKZQ=",
88 | "owner": "numtide",
89 | "repo": "flake-utils",
90 | "rev": "1ed9fb1935d260de5fe1c2f7ee0ebaae17ed2fa1",
91 | "type": "github"
92 | },
93 | "original": {
94 | "owner": "numtide",
95 | "repo": "flake-utils",
96 | "type": "github"
97 | }
98 | },
99 | "haumea": {
100 | "inputs": {
101 | "nixpkgs": [
102 | "std",
103 | "lib"
104 | ]
105 | },
106 | "locked": {
107 | "lastModified": 1685133229,
108 | "narHash": "sha256-FePm/Gi9PBSNwiDFq3N+DWdfxFq0UKsVVTJS3cQPn94=",
109 | "owner": "nix-community",
110 | "repo": "haumea",
111 | "rev": "34dd58385092a23018748b50f9b23de6266dffc2",
112 | "type": "github"
113 | },
114 | "original": {
115 | "owner": "nix-community",
116 | "ref": "v0.2.2",
117 | "repo": "haumea",
118 | "type": "github"
119 | }
120 | },
121 | "incl": {
122 | "inputs": {
123 | "nixlib": [
124 | "std",
125 | "lib"
126 | ]
127 | },
128 | "locked": {
129 | "lastModified": 1669263024,
130 | "narHash": "sha256-E/+23NKtxAqYG/0ydYgxlgarKnxmDbg6rCMWnOBqn9Q=",
131 | "owner": "divnix",
132 | "repo": "incl",
133 | "rev": "ce7bebaee048e4cd7ebdb4cee7885e00c4e2abca",
134 | "type": "github"
135 | },
136 | "original": {
137 | "owner": "divnix",
138 | "repo": "incl",
139 | "type": "github"
140 | }
141 | },
142 | "lib": {
143 | "locked": {
144 | "lastModified": 1694306727,
145 | "narHash": "sha256-26fkTOJOI65NOTNKFvtcJF9mzzf/kK9swHzfYt1Dl6Q=",
146 | "owner": "nix-community",
147 | "repo": "nixpkgs.lib",
148 | "rev": "c30b6a84c0b84ec7aecbe74466033facc9ed103f",
149 | "type": "github"
150 | },
151 | "original": {
152 | "owner": "nix-community",
153 | "repo": "nixpkgs.lib",
154 | "type": "github"
155 | }
156 | },
157 | "nixago": {
158 | "inputs": {
159 | "flake-utils": "flake-utils",
160 | "nixago-exts": [],
161 | "nixpkgs": [
162 | "nixpkgs"
163 | ]
164 | },
165 | "locked": {
166 | "lastModified": 1687381756,
167 | "narHash": "sha256-IUMIlYfrvj7Yli4H2vvyig8HEPpfCeMaE6+kBGPzFyk=",
168 | "owner": "nix-community",
169 | "repo": "nixago",
170 | "rev": "dacceb10cace103b3e66552ec9719fa0d33c0dc9",
171 | "type": "github"
172 | },
173 | "original": {
174 | "owner": "nix-community",
175 | "repo": "nixago",
176 | "type": "github"
177 | }
178 | },
179 | "nixpkgs": {
180 | "locked": {
181 | "lastModified": 1708343346,
182 | "narHash": "sha256-qlzHvterVRzS8fS0ophQpkh0rqw0abijHEOAKm0HmV0=",
183 | "owner": "nixos",
184 | "repo": "nixpkgs",
185 | "rev": "9312b935a538684049cb668885e60f15547d4c5f",
186 | "type": "github"
187 | },
188 | "original": {
189 | "owner": "nixos",
190 | "ref": "release-23.11",
191 | "repo": "nixpkgs",
192 | "type": "github"
193 | }
194 | },
195 | "nosys": {
196 | "locked": {
197 | "lastModified": 1668010795,
198 | "narHash": "sha256-JBDVBnos8g0toU7EhIIqQ1If5m/nyBqtHhL3sicdPwI=",
199 | "owner": "divnix",
200 | "repo": "nosys",
201 | "rev": "feade0141487801c71ff55623b421ed535dbdefa",
202 | "type": "github"
203 | },
204 | "original": {
205 | "owner": "divnix",
206 | "repo": "nosys",
207 | "type": "github"
208 | }
209 | },
210 | "paisano": {
211 | "inputs": {
212 | "call-flake": "call-flake",
213 | "nixpkgs": [
214 | "std",
215 | "nixpkgs"
216 | ],
217 | "nosys": "nosys",
218 | "yants": [
219 | "std",
220 | "yants"
221 | ]
222 | },
223 | "locked": {
224 | "lastModified": 1693982790,
225 | "narHash": "sha256-WTZYlqGUjzzz/PSzcvjEZz2kkwYSXObjeQVrFBaqa2Y=",
226 | "owner": "paisano-nix",
227 | "repo": "core",
228 | "rev": "3e897a19418361ece34841105122ed4f9379ca96",
229 | "type": "github"
230 | },
231 | "original": {
232 | "owner": "paisano-nix",
233 | "repo": "core",
234 | "type": "github"
235 | }
236 | },
237 | "paisano-tui": {
238 | "flake": false,
239 | "locked": {
240 | "lastModified": 1708353388,
241 | "narHash": "sha256-RzNQ5P4fdYYAXb5Bmazh5KLjlbeCtyMX7WNiOFsqN68=",
242 | "owner": "paisano-nix",
243 | "repo": "tui",
244 | "rev": "db1f97e3d5213e66e5c1251d23d1401d7068b5f5",
245 | "type": "github"
246 | },
247 | "original": {
248 | "owner": "paisano-nix",
249 | "ref": "v0.4.3",
250 | "repo": "tui",
251 | "type": "github"
252 | }
253 | },
254 | "root": {
255 | "inputs": {
256 | "devshell": "devshell",
257 | "nixago": "nixago",
258 | "nixpkgs": "nixpkgs",
259 | "std": "std"
260 | }
261 | },
262 | "std": {
263 | "inputs": {
264 | "arion": [
265 | "std",
266 | "blank"
267 | ],
268 | "blank": "blank",
269 | "devshell": [
270 | "devshell"
271 | ],
272 | "dmerge": "dmerge",
273 | "haumea": "haumea",
274 | "incl": "incl",
275 | "lib": "lib",
276 | "makes": [
277 | "std",
278 | "blank"
279 | ],
280 | "microvm": [
281 | "std",
282 | "blank"
283 | ],
284 | "n2c": [
285 | "std",
286 | "blank"
287 | ],
288 | "nixago": [
289 | "nixago"
290 | ],
291 | "nixpkgs": [
292 | "nixpkgs"
293 | ],
294 | "paisano": "paisano",
295 | "paisano-tui": "paisano-tui",
296 | "terranix": [
297 | "std",
298 | "blank"
299 | ],
300 | "yants": "yants"
301 | },
302 | "locked": {
303 | "lastModified": 1708473028,
304 | "narHash": "sha256-z5/UbogAnRVlyT2qZPqUUb5yPLgGxO+iWAy4pxBjDAQ=",
305 | "owner": "divnix",
306 | "repo": "std",
307 | "rev": "151ecce8eecc8d0fd15d9fcd75d59a45ebdcd7ee",
308 | "type": "github"
309 | },
310 | "original": {
311 | "owner": "divnix",
312 | "repo": "std",
313 | "type": "github"
314 | }
315 | },
316 | "systems": {
317 | "locked": {
318 | "lastModified": 1681028828,
319 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
320 | "owner": "nix-systems",
321 | "repo": "default",
322 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
323 | "type": "github"
324 | },
325 | "original": {
326 | "owner": "nix-systems",
327 | "repo": "default",
328 | "type": "github"
329 | }
330 | },
331 | "yants": {
332 | "inputs": {
333 | "nixpkgs": [
334 | "std",
335 | "lib"
336 | ]
337 | },
338 | "locked": {
339 | "lastModified": 1686863218,
340 | "narHash": "sha256-kooxYm3/3ornWtVBNHM3Zh020gACUyFX2G0VQXnB+mk=",
341 | "owner": "divnix",
342 | "repo": "yants",
343 | "rev": "8f0da0dba57149676aa4817ec0c880fbde7a648d",
344 | "type": "github"
345 | },
346 | "original": {
347 | "owner": "divnix",
348 | "repo": "yants",
349 | "type": "github"
350 | }
351 | }
352 | },
353 | "root": "root",
354 | "version": 7
355 | }
356 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "Paisano's TUI/CLI companion";
3 |
4 | inputs.nixpkgs.url = "github:nixos/nixpkgs/release-23.11";
5 |
6 | inputs.std = {
7 | url = "github:divnix/std";
8 | inputs.nixpkgs.follows = "nixpkgs";
9 | inputs.devshell.follows = "devshell";
10 | inputs.nixago.follows = "nixago";
11 | };
12 |
13 | inputs.devshell = {
14 | url = "github:numtide/devshell";
15 | inputs.nixpkgs.follows = "nixpkgs";
16 | };
17 |
18 | inputs.nixago = {
19 | url = "github:nix-community/nixago";
20 | inputs.nixpkgs.follows = "nixpkgs";
21 | inputs.nixago-exts.follows = "";
22 | };
23 |
24 | outputs = {
25 | std,
26 | self,
27 | ...
28 | } @ inputs:
29 | std.growOn {
30 | inherit inputs;
31 | cellsFrom = ./nix;
32 | cellBlocks = with std.blockTypes; [
33 | # Development Environments
34 | (nixago "config")
35 | (devshells "shells")
36 | # Application Development
37 | (installables "app")
38 | ];
39 | }
40 | {
41 | packages = std.harvest self ["tui" "app"];
42 | devShells = std.harvest self ["repo" "shells"];
43 | };
44 | }
45 |
--------------------------------------------------------------------------------
/mdbook-paisano-preprocessor.css:
--------------------------------------------------------------------------------
1 | /*
2 | CSS for rendering the Standard Refrerence page
3 |
4 | You can modify it to customize the representation.
5 | */
6 |
7 | /* Cell Reference */
8 | div.std-cell.std-no-readme {
9 | display: block;
10 | }
11 |
12 | div.std-cell div.std-readme-md {
13 | /* CSS rules */
14 | }
15 |
16 | div.std-cell h2 {
17 | /* CSS rules */
18 | }
19 |
20 | div.std-cell h2 {
21 | /* CSS rules */
22 | }
23 |
24 | /* Block Reference */
25 | div.std-block.std-no-readme {
26 | display: block;
27 | }
28 |
29 | div.std-block div.std-readme-md {
30 | /* CSS rules */
31 | }
32 |
33 | div.std-cell h3 {
34 | /* CSS rules */
35 | }
36 |
37 | /* Target Reference */
38 | div.std-target.std-no-readme {
39 | display: none;
40 | }
41 |
42 | div.std-target div.std-readme-md {
43 | /* CSS rules */
44 | }
45 |
46 | div.std-target h4 {
47 | /* CSS rules */
48 | }
49 |
50 | div.std-target h5 {
51 | /* CSS rules */
52 | }
53 |
--------------------------------------------------------------------------------
/nix/repo/config.nix:
--------------------------------------------------------------------------------
1 | /*
2 | This file holds configuration data for repo dotfiles.
3 |
4 | Q: Why not just put the put the file there?
5 |
6 | A: (1) dotfile proliferation
7 | (2) have all the things in one place / fromat
8 | (3) potentially share / re-use configuration data - keeping it in sync
9 | */
10 | let
11 | inherit (inputs) nixpkgs;
12 | inherit (inputs.std.inputs) dmerge;
13 | inherit (inputs.std.data) configs;
14 | inherit (inputs.std.lib.dev) mkNixago;
15 | in {
16 | # Tool Homepage: https://numtide.github.io/treefmt/
17 | treefmt = (mkNixago configs.treefmt) {
18 | packages = [nixpkgs.go];
19 | data = {
20 | formatter = {
21 | go = {
22 | command = "gofmt";
23 | options = ["-w"];
24 | includes = ["*.go"];
25 | };
26 | };
27 | };
28 | };
29 |
30 | # Tool Homepage: https://editorconfig.org/
31 | editorconfig = (mkNixago configs.editorconfig) {};
32 | conform = (mkNixago configs.conform) {};
33 |
34 | # Tool Homepage: https://github.com/evilmartians/lefthook
35 | lefthook = (mkNixago configs.lefthook) {};
36 |
37 | cog = (mkNixago configs.cog) {
38 | data = {
39 | changelog = {
40 | remote = "github.com";
41 | repository = "tui";
42 | owner = "paisano-nix";
43 | };
44 | post_bump_hooks = dmerge.append [
45 | "echo Go to and post: https://discourse.nixos.org/t/paisano-tui-cli/27351"
46 | ];
47 | };
48 | };
49 |
50 | # Tool Hompeage: https://github.com/apps/settings
51 | # Install Setting App in your repo to enable it
52 | githubsettings = (mkNixago configs.githubsettings) {
53 | data = {
54 | repository = {
55 | name = "tui";
56 | inherit (import (inputs.self + /flake.nix)) description;
57 | homepage = "https://paisano-nix.github.io/tui";
58 | topics = "nix, nix-flakes, flake, ux, tui, cli";
59 | default_branch = "main";
60 | allow_squash_merge = true;
61 | allow_merge_commit = false;
62 | allow_rebase_merge = true;
63 | delete_branch_on_merge = true;
64 | private = false;
65 | has_issues = true;
66 | has_projects = false;
67 | has_wiki = false;
68 | has_downloads = false;
69 | };
70 | };
71 | };
72 |
73 | # Tool Homepage: https://rust-lang.github.io/mdBook/
74 | mdbook = (mkNixago configs.mdbook) {
75 | # add preprocessor packages here
76 | packages = [nixpkgs.mdbook-linkcheck];
77 | data = {
78 | # Configuration Reference: https://rust-lang.github.io/mdBook/format/configuration/index.html
79 | book.title = "Paisano TUI Book";
80 | preprocessor.paisano-preprocessor = {
81 | multi = [
82 | {
83 | chapter = "TUI Reference";
84 | cell = "tui";
85 | }
86 | ];
87 | };
88 | output = {
89 | html = {
90 | additional-css = ["./mdbook-paisano-preprocessor.css"];
91 | };
92 | # Tool Homepage: https://github.com/Michael-F-Bryan/mdbook-linkcheck
93 | linkcheck = {};
94 | };
95 | };
96 | };
97 | }
98 |
--------------------------------------------------------------------------------
/nix/repo/shells.nix:
--------------------------------------------------------------------------------
1 | /*
2 | This file holds reproducible shells with commands in them.
3 |
4 | They conveniently also generate config files in their startup hook.
5 | */
6 | let
7 | inherit (inputs) nixpkgs;
8 | inherit (inputs.std.lib.dev) mkShell;
9 | inherit (inputs.nixpkgs.lib) mapAttrs optionals;
10 | # Tool Homepage: https://numtide.github.io/devshell/
11 | in
12 | mapAttrs (_: mkShell) rec {
13 | mdbook.nixago = [cell.config.mdbook];
14 | default = {
15 | name = "Paisano TUI";
16 |
17 | # Tool Homepage: https://nix-community.github.io/nixago/
18 | # This is Standard's devshell integration.
19 | # It runs the startup hook when entering the shell.
20 | nixago = [
21 | cell.config.conform
22 | cell.config.treefmt
23 | cell.config.editorconfig
24 | cell.config.githubsettings
25 | cell.config.lefthook
26 | cell.config.mdbook
27 | cell.config.cog
28 | ];
29 |
30 | commands =
31 | [
32 | {
33 | package = nixpkgs.delve;
34 | category = "dev";
35 | name = "dlv";
36 | }
37 | {
38 | package = nixpkgs.go;
39 | category = "dev";
40 | }
41 | {
42 | package = nixpkgs.gotools;
43 | category = "dev";
44 | }
45 | {
46 | package = nixpkgs.gopls;
47 | category = "dev";
48 | }
49 | ]
50 | ++ optionals nixpkgs.stdenv.isLinux [
51 | {
52 | package = nixpkgs.golangci-lint;
53 | category = "dev";
54 | }
55 | ];
56 | };
57 | }
58 |
--------------------------------------------------------------------------------
/nix/tui/app.md:
--------------------------------------------------------------------------------
1 | # `paisano` CLI / TUI
2 |
3 | ```console
4 | ❯ paisano --help
5 | paisano is the CLI / TUI companion for Paisano.
6 |
7 | - Invoke without any arguments to start the TUI.
8 | - Invoke with a target spec and action to run a known target's action directly.
9 |
10 | Enable autocompletion via 'paisano _carapace '.
11 | For more instructions, see: https://rsteube.github.io/carapace/carapace/gen/hiddenSubcommand.html
12 |
13 | Usage:
14 | paisano //[cell]/[block]/[target]:[action] [args...]
15 | paisano [command]
16 |
17 | Available Commands:
18 | check Validate the repository.
19 | list List available targets.
20 | re-cache Refresh the CLI cache.
21 |
22 | Flags:
23 | --for string system, for which the target will be built (e.g. 'x86_64-linux')
24 | -h, --help help for paisano
25 | -v, --version version for paisano
26 |
27 | Use "paisano [command] --help" for more information about a command.
28 |
29 | ```
30 |
--------------------------------------------------------------------------------
/nix/tui/app.nix:
--------------------------------------------------------------------------------
1 | let
2 | version = "0.15.0+dev";
3 |
4 | inherit (inputs) nixpkgs;
5 | inherit (nixpkgs.lib) licenses;
6 | in {
7 | default = cell.app.paisano;
8 |
9 | paisano = nixpkgs.buildGoModule rec {
10 | inherit version;
11 | pname = "paisano";
12 | meta = {
13 | inherit (import (inputs.self + /flake.nix)) description;
14 | license = licenses.unlicense;
15 | homepage = "https://github.com/paisano-nix/tui";
16 | };
17 |
18 | src = inputs.self + /src;
19 |
20 | vendorHash = "sha256-S1oPselqHRIPcqDSsvdIkCwu1siQGRDHOkxWtYwa+g4=";
21 |
22 | nativeBuildInputs = [nixpkgs.installShellFiles];
23 |
24 | postInstall = ''
25 | installShellCompletion --cmd paisano \
26 | --bash <($out/bin/paisano _carapace bash) \
27 | --fish <($out/bin/paisano _carapace fish) \
28 | --zsh <($out/bin/paisano _carapace zsh)
29 | '';
30 |
31 | ldflags = [
32 | "-s"
33 | "-w"
34 | "-X main.buildVersion=${version}"
35 | ];
36 | };
37 | }
38 |
--------------------------------------------------------------------------------
/src/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 |
--------------------------------------------------------------------------------
/src/cache/cache.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2021, 2022 Tamás Gulácsi
2 | // SPDX-FileCopyrightText: 2017 The Go Authors. All rights reserved.
3 | //
4 | // SPDX-License-Identifier: BSD-3-Clause
5 |
6 | // Package filecache implements an artifact cache.
7 | //
8 | // It is copied from Go's cmd/go/internal/cache,
9 | // cleared the Go-specific environment settings for default cache,
10 | // Go version-salted hash, and added TrimWithLimits.
11 | package cache
12 |
13 | import (
14 | "bytes"
15 | "crypto/sha256"
16 | "encoding/hex"
17 | "errors"
18 | "fmt"
19 | "io"
20 | "io/fs"
21 | "os"
22 | "path/filepath"
23 | "strconv"
24 | "strings"
25 | "time"
26 |
27 | "github.com/google/renameio/v2"
28 | "github.com/rogpeppe/go-internal/lockedfile"
29 | )
30 |
31 | // An ActionID is a cache action key, the hash of a complete description of a
32 | // repeatable computation (command line, environment variables,
33 | // input file contents, executable contents).
34 | type ActionID ID
35 |
36 | // NewActionID returns the hashed bytes.
37 | func NewActionID(p []byte) ActionID { return sha256.Sum256(p) }
38 |
39 | // An OutputID is a cache output key, the hash of an output of a computation.
40 | type OutputID ID
41 |
42 | // A Cache is a package cache, backed by a file system directory tree.
43 | type Cache struct {
44 | dir string
45 | now func() time.Time
46 |
47 | mtimeInterval time.Duration
48 | }
49 |
50 | // Open opens and returns the cache in the given directory.
51 | //
52 | // It is safe for multiple processes on a single machine to use the
53 | // same cache directory in a local file system simultaneously.
54 | // They will coordinate using operating system file locks and may
55 | // duplicate effort but will not corrupt the cache.
56 | //
57 | // However, it is NOT safe for multiple processes on different machines
58 | // to share a cache directory (for example, if the directory were stored
59 | // in a network file system). File locking is notoriously unreliable in
60 | // network file systems and may not suffice to protect the cache.
61 | func Open(dir string) (*Cache, error) {
62 | info, err := os.Stat(dir)
63 | if err != nil {
64 | return nil, err
65 | }
66 | if !info.IsDir() {
67 | return nil, &fs.PathError{Op: "open", Path: dir, Err: fmt.Errorf("not a directory")}
68 | }
69 | for i := 0; i < 256; i++ {
70 | name := filepath.Join(dir, fmt.Sprintf("%02x", i))
71 | // nosemgrep: go.lang.correctness.permissions.file_permission.incorrect-default-permission
72 | if err := os.MkdirAll(name, 0770); err != nil {
73 | return nil, err
74 | }
75 | }
76 | c := &Cache{
77 | dir: dir,
78 | now: time.Now,
79 | mtimeInterval: DefaultMTimeInterval,
80 | }
81 | return c, nil
82 | }
83 |
84 | // SetMTimeInterval set the time precision for updating file access times.
85 | // The default is 1 hour.
86 | func (c *Cache) SetMTimeInterval(d time.Duration) {
87 | if d <= 0 {
88 | d = DefaultMTimeInterval
89 | }
90 | c.mtimeInterval = d
91 | }
92 |
93 | // fileName returns the name of the file corresponding to the given id.
94 | func (c *Cache) fileName(id [HashSize]byte, key string) string {
95 | return filepath.Join(c.dir, fmt.Sprintf("%02x", id[0]), fmt.Sprintf("%x", id)+"-"+key)
96 | }
97 |
98 | // An entryNotFoundError indicates that a cache entry was not found, with an
99 | // optional underlying reason.
100 | type entryNotFoundError struct {
101 | Err error
102 | }
103 |
104 | func (e *entryNotFoundError) Error() string {
105 | if e.Err == nil {
106 | return "cache entry not found"
107 | }
108 | return fmt.Sprintf("cache entry not found: %v", e.Err)
109 | }
110 |
111 | func (e *entryNotFoundError) Unwrap() error {
112 | return e.Err
113 | }
114 |
115 | const (
116 | // action entry file is "v1 \n"
117 | hexSize = HashSize * 2
118 | entrySize = 2 + 1 + hexSize + 1 + hexSize + 1 + 20 + 1 + 20 + 1
119 | )
120 |
121 | type Entry struct {
122 | OutputID OutputID
123 | Size int64
124 | Time time.Time
125 | }
126 |
127 | // Get looks up the action ID in the cache,
128 | // returning the corresponding output ID and file size, if any.
129 | // Note that finding an output ID does not guarantee that the
130 | // saved file for that output ID is still available.
131 | func (c *Cache) Get(id ActionID) (Entry, error) {
132 | missing := func(reason error) (Entry, error) {
133 | return Entry{}, &entryNotFoundError{Err: reason}
134 | }
135 | f, err := os.Open(c.fileName(id, "a"))
136 | if err != nil {
137 | return missing(err)
138 | }
139 | defer f.Close()
140 | entry := make([]byte, entrySize+1) // +1 to detect whether f is too long
141 | if n, err := io.ReadFull(f, entry); n > entrySize {
142 | return missing(errors.New("too long"))
143 | } else if !errors.Is(err, io.ErrUnexpectedEOF) {
144 | if errors.Is(err, io.EOF) {
145 | return missing(errors.New("file is empty"))
146 | }
147 | return missing(err)
148 | } else if n < entrySize {
149 | return missing(errors.New("entry file incomplete"))
150 | }
151 | if entry[0] != 'v' || entry[1] != '1' || entry[2] != ' ' || entry[3+hexSize] != ' ' || entry[3+hexSize+1+hexSize] != ' ' || entry[3+hexSize+1+hexSize+1+20] != ' ' || entry[entrySize-1] != '\n' {
152 | return missing(errors.New("invalid header"))
153 | }
154 | eid, entry := entry[3:3+hexSize], entry[3+hexSize:]
155 | eout, entry := entry[1:1+hexSize], entry[1+hexSize:]
156 | esize, entry := entry[1:1+20], entry[1+20:]
157 | etime, _ := entry[1:1+20], entry[1+20:]
158 | var buf [HashSize]byte
159 | if _, err := hex.Decode(buf[:], eid); err != nil {
160 | return missing(fmt.Errorf("decoding ID: %w", err))
161 | } else if buf != id {
162 | return missing(errors.New("mismatched ID"))
163 | }
164 | if _, err := hex.Decode(buf[:], eout); err != nil {
165 | return missing(fmt.Errorf("decoding output ID: %w", err))
166 | }
167 | i := 0
168 | for i < len(esize) && esize[i] == ' ' {
169 | i++
170 | }
171 | size, err := strconv.ParseInt(string(esize[i:]), 10, 64)
172 | if err != nil {
173 | return missing(fmt.Errorf("parsing size: %w", err))
174 | } else if size < 0 {
175 | return missing(errors.New("negative size"))
176 | }
177 | i = 0
178 | for i < len(etime) && etime[i] == ' ' {
179 | i++
180 | }
181 | tm, err := strconv.ParseInt(string(etime[i:]), 10, 64)
182 | if err != nil {
183 | return missing(fmt.Errorf("parsing timestamp: %w", err))
184 | } else if tm < 0 {
185 | return missing(errors.New("negative timestamp"))
186 | }
187 |
188 | c.used(c.fileName(id, "a"))
189 |
190 | return Entry{buf, size, time.Unix(0, tm)}, nil
191 | }
192 |
193 | // GetFile looks up the action ID in the cache and returns
194 | // the name of the corresponding data file.
195 | func (c *Cache) GetFile(id ActionID) (file string, entry Entry, err error) {
196 | entry, err = c.Get(id)
197 | if err != nil {
198 | return "", Entry{}, err
199 | }
200 | file = c.OutputFile(entry.OutputID)
201 | info, err := os.Stat(file)
202 | if err != nil {
203 | return "", Entry{}, &entryNotFoundError{Err: err}
204 | }
205 | if info.Size() != entry.Size {
206 | return "", Entry{}, &entryNotFoundError{Err: errors.New("file incomplete")}
207 | }
208 | return file, entry, nil
209 | }
210 |
211 | // GetBytes looks up the action ID in the cache and returns
212 | // the corresponding output bytes.
213 | // GetBytes should only be used for data that can be expected to fit in memory.
214 | func (c *Cache) GetBytes(id ActionID) ([]byte, Entry, error) {
215 | entry, err := c.Get(id)
216 | if err != nil {
217 | return nil, entry, err
218 | }
219 | data, _ := lockedfile.Read(c.OutputFile(entry.OutputID))
220 | if sha256.Sum256(data) != entry.OutputID {
221 | return nil, entry, &entryNotFoundError{Err: errors.New("bad checksum")}
222 | }
223 | return data, entry, nil
224 | }
225 |
226 | // OutputFile returns the name of the cache file storing output with the given OutputID.
227 | func (c *Cache) OutputFile(out OutputID) string {
228 | file := c.fileName(out, "d")
229 | c.used(file)
230 | return file
231 | }
232 |
233 | // Time constants for cache expiration.
234 | //
235 | // We set the mtime on a cache file on each use, but at most one per mtimeInterval (1 hour),
236 | // to avoid causing many unnecessary inode updates. The mtimes therefore
237 | // roughly reflect "time of last use" but may in fact be older by at most an hour.
238 | //
239 | // We scan the cache for entries to delete at most once per trimInterval (1 day).
240 | //
241 | // When we do scan the cache, we delete entries that have not been used for
242 | // at least trimLimit (5 days). Statistics gathered from a month of usage by
243 | // Go developers found that essentially all reuse of cached entries happened
244 | // within 5 days of the previous reuse. See golang.org/issue/22990.
245 | const (
246 | DefaultMTimeInterval = 1 * time.Hour
247 | DefaultTrimInterval = 24 * time.Hour
248 | DefaultTrimLimit = 5 * 24 * time.Hour
249 | )
250 |
251 | // used makes a best-effort attempt to update mtime on file,
252 | // so that mtime reflects cache access time.
253 | //
254 | // Because the reflection only needs to be approximate,
255 | // and to reduce the amount of disk activity caused by using
256 | // cache entries, used only updates the mtime if the current
257 | // mtime is more than an hour old. This heuristic eliminates
258 | // nearly all of the mtime updates that would otherwise happen,
259 | // while still keeping the mtimes useful for cache trimming.
260 | func (c *Cache) used(file string) {
261 | info, err := os.Stat(file)
262 | if err == nil && c.now().Sub(info.ModTime()) < c.mtimeInterval {
263 | return
264 | }
265 | _ = os.Chtimes(file, c.now(), c.now())
266 | }
267 |
268 | // Trim removes old cache entries that are likely not to be reused.
269 | // It uses the default trim interval and limit.
270 | func (c *Cache) Trim() {
271 | c.TrimWithLimit(0, 0)
272 | }
273 |
274 | // TrimLimited removes old cache entries that are likely not to be reused.
275 | //
276 | // For each duration, <=0 means Default.
277 | func (c *Cache) TrimWithLimit(trimInterval, trimLimit time.Duration) {
278 | if trimInterval <= 0 {
279 | trimInterval = DefaultTrimInterval
280 | }
281 | if trimLimit <= 0 {
282 | trimLimit = DefaultTrimLimit
283 | }
284 | now := c.now()
285 |
286 | // We maintain in dir/trim.txt the time of the last completed cache trim.
287 | // If the cache has been trimmed recently enough, do nothing.
288 | // This is the common case.
289 | // If the trim file is corrupt, detected if the file can't be parsed, or the
290 | // trim time is too far in the future, attempt the trim anyway. It's possible that
291 | // the cache was full when the corruption happened. Attempting a trim on
292 | // an empty cache is cheap, so there wouldn't be a big performance hit in that case.
293 | if data, err := os.ReadFile(filepath.Join(c.dir, "trim.txt")); err == nil {
294 | if t, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64); err == nil {
295 | lastTrim := time.Unix(t, 0)
296 | if d := now.Sub(lastTrim); d < trimInterval && d > -c.mtimeInterval {
297 | return
298 | }
299 | }
300 | }
301 |
302 | // Trim each of the 256 subdirectories.
303 | // We subtract an additional mtimeInterval
304 | // to account for the imprecision of our "last used" mtimes.
305 | cutoff := now.Add(-trimLimit - c.mtimeInterval)
306 | for i := 0; i < 256; i++ {
307 | subdir := filepath.Join(c.dir, fmt.Sprintf("%02x", i))
308 | c.trimSubdir(subdir, cutoff)
309 | }
310 |
311 | // Ignore errors from here: if we don't write the complete timestamp, the
312 | // cache will appear older than it is, and we'll trim it again next time.
313 | var b bytes.Buffer
314 | fmt.Fprintf(&b, "%d", now.Unix())
315 | if err := lockedfile.Write(filepath.Join(c.dir, "trim.txt"), &b, 0666); err != nil {
316 | return
317 | }
318 | }
319 |
320 | // trimSubdir trims a single cache subdirectory.
321 | func (c *Cache) trimSubdir(subdir string, cutoff time.Time) {
322 | // Read all directory entries from subdir before removing
323 | // any files, in case removing files invalidates the file offset
324 | // in the directory scan. Also, ignore error from f.Readdirnames,
325 | // because we don't care about reporting the error and we still
326 | // want to process any entries found before the error.
327 | f, err := os.Open(subdir)
328 | if err != nil {
329 | return
330 | }
331 | names, _ := f.Readdirnames(-1)
332 | f.Close()
333 |
334 | for _, name := range names {
335 | // Remove only cache entries (xxxx-a and xxxx-d).
336 | if !strings.HasSuffix(name, "-a") && !strings.HasSuffix(name, "-d") {
337 | continue
338 | }
339 | entry := filepath.Join(subdir, name)
340 | info, err := os.Stat(entry)
341 | if err == nil && info.ModTime().Before(cutoff) {
342 | os.Remove(entry)
343 | }
344 | }
345 | }
346 |
347 | // putIndexEntry adds an entry to the cache recording that executing the action
348 | // with the given id produces an output with the given output id (hash) and size.
349 | func (c *Cache) putIndexEntry(id ActionID, out OutputID, size int64) error {
350 | // Note: We expect that for one reason or another it may happen
351 | // that repeating an action produces a different output hash
352 | // (for example, if the output contains a time stamp or temp dir name).
353 | // While not ideal, this is also not a correctness problem, so we
354 | // don't make a big deal about it. In particular, we leave the action
355 | // cache entries writable specifically so that they can be overwritten.
356 | //
357 | entry := fmt.Sprintf("v1 %x %x %20d %20d\n", id, out, size, time.Now().UnixNano())
358 | file := c.fileName(id, "a")
359 |
360 | // Copy file to cache directory.
361 | if err := renameio.WriteFile(file, []byte(entry), 0666); err != nil {
362 | return err
363 | }
364 | _ = os.Chtimes(file, c.now(), c.now()) // mainly for tests
365 |
366 | return nil
367 | }
368 |
369 | // Put stores the given output in the cache as the output for the action ID.
370 | // It may read file twice. The content of file must not change between the two passes.
371 | func (c *Cache) Put(id ActionID, file io.ReadSeeker) (OutputID, int64, error) {
372 | // Compute output ID.
373 | h := NewHash()
374 | if _, err := file.Seek(0, 0); err != nil {
375 | return OutputID{}, 0, err
376 | }
377 | size, err := io.Copy(h, file)
378 | if err != nil {
379 | return OutputID{}, 0, err
380 | }
381 | out := OutputID(h.SumID())
382 |
383 | // Copy to cached output file (if not already present).
384 | if err := c.copyFile(file, out, size); err != nil {
385 | return out, size, err
386 | }
387 |
388 | // Add to cache index.
389 | return out, size, c.putIndexEntry(id, out, size)
390 | }
391 |
392 | // PutBytes stores the given bytes in the cache as the output for the action ID.
393 | func (c *Cache) PutBytes(id ActionID, data []byte) error {
394 | _, _, err := c.Put(id, bytes.NewReader(data))
395 | return err
396 | }
397 |
398 | // copyFile copies file into the cache, expecting it to have the given
399 | // output ID and size, if that file is not present already.
400 | func (c *Cache) copyFile(file io.ReadSeeker, out OutputID, size int64) error {
401 | name := c.fileName(out, "d")
402 | info, err := os.Stat(name)
403 | if err == nil && info.Size() == size {
404 | // Check hash.
405 | if f, err := os.Open(name); err == nil {
406 | h := NewHash()
407 | _, _ = io.Copy(h, f)
408 | _ = f.Close()
409 | out2 := OutputID(h.SumID())
410 | if out == out2 {
411 | return nil
412 | }
413 | }
414 | // Hash did not match. Fall through and rewrite file.
415 | }
416 |
417 | // Copy file to cache directory.
418 | mode := os.O_RDWR | os.O_CREATE
419 | if err == nil && info.Size() > size { // shouldn't happen but fix in case
420 | mode |= os.O_TRUNC
421 | }
422 | // nosemgrep: go.lang.correctness.permissions.file_permission.incorrect-default-permission
423 | f, err := os.OpenFile(name, mode, 0660)
424 | if err != nil {
425 | return err
426 | }
427 | defer f.Close()
428 | if size == 0 {
429 | // File now exists with correct size.
430 | // Only one possible zero-length file, so contents are OK too.
431 | // Early return here makes sure there's a "last byte" for code below.
432 | return nil
433 | }
434 |
435 | // From here on, if any of the I/O writing the file fails,
436 | // we make a best-effort attempt to truncate the file f
437 | // before returning, to avoid leaving bad bytes in the file.
438 |
439 | // Copy file to f, but also into h to double-check hash.
440 | if _, err := file.Seek(0, 0); err != nil {
441 | _ = f.Truncate(0)
442 | return err
443 | }
444 | h := NewHash()
445 | w := io.MultiWriter(f, h)
446 | if _, err := io.CopyN(w, file, size-1); err != nil {
447 | _ = f.Truncate(0)
448 | return err
449 | }
450 | // Check last byte before writing it; writing it will make the size match
451 | // what other processes expect to find and might cause them to start
452 | // using the file.
453 | buf := make([]byte, 1)
454 | if _, err := file.Read(buf); err != nil {
455 | _ = f.Truncate(0)
456 | return err
457 | }
458 | h.Write(buf)
459 | sum := h.Sum(nil)
460 | if !bytes.Equal(sum, out[:]) {
461 | _ = f.Truncate(0)
462 | return fmt.Errorf("file content changed underfoot")
463 | }
464 |
465 | // Commit cache file entry.
466 | if _, err := f.Write(buf); err != nil {
467 | _ = f.Truncate(0)
468 | return err
469 | }
470 | if err := f.Close(); err != nil {
471 | // Data might not have been written,
472 | // but file may look like it is the right size.
473 | // To be extra careful, remove cached file.
474 | _ = os.Remove(name)
475 | return err
476 | }
477 | _ = os.Chtimes(name, c.now(), c.now()) // mainly for tests
478 |
479 | return nil
480 | }
481 |
--------------------------------------------------------------------------------
/src/cache/cache_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2021, 2022 Tamás Gulácsi
2 | // SPDX-FileCopyrightText: 2017 The Go Authors. All rights reserved.
3 | //
4 | // SPDX-License-Identifier: BSD-3-Clause
5 |
6 | package cache
7 |
8 | import (
9 | "bytes"
10 | "encoding/binary"
11 | "fmt"
12 | "os"
13 | "path/filepath"
14 | "testing"
15 | "time"
16 | )
17 |
18 | func TestBasic(t *testing.T) {
19 | dir, err := os.MkdirTemp("", "cachetest-")
20 | if err != nil {
21 | t.Fatal(err)
22 | }
23 | defer os.RemoveAll(dir)
24 | _, err = Open(filepath.Join(dir, "notexist"))
25 | if err == nil {
26 | t.Fatal(`Open("tmp/notexist") succeeded, want failure`)
27 | }
28 |
29 | cdir := filepath.Join(dir, "c1")
30 | if err := os.Mkdir(cdir, 0777); err != nil {
31 | t.Fatal(err)
32 | }
33 |
34 | c1, err := Open(cdir)
35 | if err != nil {
36 | t.Fatalf("Open(c1) (create): %v", err)
37 | }
38 | if err := c1.putIndexEntry(dummyID(1), dummyID(12), 13); err != nil {
39 | t.Fatalf("addIndexEntry: %v", err)
40 | }
41 | if err := c1.putIndexEntry(dummyID(1), dummyID(2), 3); err != nil { // overwrite entry
42 | t.Fatalf("addIndexEntry: %v", err)
43 | }
44 | if entry, err := c1.Get(dummyID(1)); err != nil || entry.OutputID != dummyID(2) || entry.Size != 3 {
45 | t.Fatalf("c1.Get(1) = %x, %v, %v, want %x, %v, nil", entry.OutputID, entry.Size, err, dummyID(2), 3)
46 | }
47 |
48 | c2, err := Open(cdir)
49 | if err != nil {
50 | t.Fatalf("Open(c2) (reuse): %v", err)
51 | }
52 | if entry, err := c2.Get(dummyID(1)); err != nil || entry.OutputID != dummyID(2) || entry.Size != 3 {
53 | t.Fatalf("c2.Get(1) = %x, %v, %v, want %x, %v, nil", entry.OutputID, entry.Size, err, dummyID(2), 3)
54 | }
55 | if err := c2.putIndexEntry(dummyID(2), dummyID(3), 4); err != nil {
56 | t.Fatalf("addIndexEntry: %v", err)
57 | }
58 | if entry, err := c1.Get(dummyID(2)); err != nil || entry.OutputID != dummyID(3) || entry.Size != 4 {
59 | t.Fatalf("c1.Get(2) = %x, %v, %v, want %x, %v, nil", entry.OutputID, entry.Size, err, dummyID(3), 4)
60 | }
61 | }
62 |
63 | func BenchmarkGrowth(t *testing.B) {
64 | dir, err := os.MkdirTemp("", "cachetest-")
65 | if err != nil {
66 | t.Fatal(err)
67 | }
68 | defer os.RemoveAll(dir)
69 |
70 | c, err := Open(dir)
71 | if err != nil {
72 | t.Fatalf("Open: %v", err)
73 | }
74 |
75 | // previously 40s (2/3 of build time) to check for this on a laptop
76 | // Now, run this with `go test -bench=.`
77 | n := t.N
78 | if testing.Short() {
79 | n = 10
80 | }
81 |
82 | for i := 0; i < n; i++ {
83 | if err := c.putIndexEntry(dummyID(i), dummyID(i*99), int64(i)*101); err != nil {
84 | t.Fatalf("addIndexEntry: %v", err)
85 | }
86 | id := ActionID(dummyID(i))
87 | entry, err := c.Get(id)
88 | if err != nil {
89 | t.Fatalf("Get(%x): %v", id, err)
90 | }
91 | if entry.OutputID != dummyID(i*99) || entry.Size != int64(i)*101 {
92 | t.Errorf("Get(%x) = %x, %d, want %x, %d", id, entry.OutputID, entry.Size, dummyID(i*99), int64(i)*101)
93 | }
94 | }
95 | for i := 0; i < n; i++ {
96 | id := ActionID(dummyID(i))
97 | entry, err := c.Get(id)
98 | if err != nil {
99 | t.Fatalf("Get2(%x): %v", id, err)
100 | }
101 | if entry.OutputID != dummyID(i*99) || entry.Size != int64(i)*101 {
102 | t.Errorf("Get2(%x) = %x, %d, want %x, %d", id, entry.OutputID, entry.Size, dummyID(i*99), int64(i)*101)
103 | }
104 | }
105 | }
106 |
107 | func dummyID(x int) [HashSize]byte {
108 | var out [HashSize]byte
109 | binary.LittleEndian.PutUint64(out[:], uint64(x))
110 | return out
111 | }
112 |
113 | func TestCacheTrim(t *testing.T) {
114 | dir, err := os.MkdirTemp("", "cachetest-")
115 | if err != nil {
116 | t.Fatal(err)
117 | }
118 | defer os.RemoveAll(dir)
119 |
120 | c, err := Open(dir)
121 | if err != nil {
122 | t.Fatalf("Open: %v", err)
123 | }
124 | const start = 1000000000
125 | now := int64(start)
126 | c.now = func() time.Time { return time.Unix(now, 0) }
127 |
128 | checkTime := func(name string, mtime int64) {
129 | t.Helper()
130 | file := filepath.Join(c.dir, name[:2], name)
131 | info, err := os.Stat(file)
132 | if err != nil {
133 | t.Fatal(err)
134 | }
135 | if info.ModTime().Unix() != mtime {
136 | t.Fatalf("%s mtime = %d, want %d", name, info.ModTime().Unix(), mtime)
137 | }
138 | }
139 |
140 | id := ActionID(dummyID(1))
141 | _ = c.PutBytes(id, []byte("abc"))
142 | entry, _ := c.Get(id)
143 | _ = c.PutBytes(ActionID(dummyID(2)), []byte("def"))
144 | mtime := now
145 | checkTime(fmt.Sprintf("%x-a", id), mtime)
146 | checkTime(fmt.Sprintf("%x-d", entry.OutputID), mtime)
147 |
148 | // Get should not change recent mtimes.
149 | now = start + 10
150 | _, _ = c.Get(id)
151 | checkTime(fmt.Sprintf("%x-a", id), mtime)
152 | checkTime(fmt.Sprintf("%x-d", entry.OutputID), mtime)
153 |
154 | // Get should change distant mtimes.
155 | now = start + 5000
156 | mtime2 := now
157 | if _, err := c.Get(id); err != nil {
158 | t.Fatal(err)
159 | }
160 | c.OutputFile(entry.OutputID)
161 | checkTime(fmt.Sprintf("%x-a", id), mtime2)
162 | checkTime(fmt.Sprintf("%x-d", entry.OutputID), mtime2)
163 |
164 | // Trim should leave everything alone: it's all too new.
165 | c.Trim()
166 | if _, err := c.Get(id); err != nil {
167 | t.Fatal(err)
168 | }
169 | c.OutputFile(entry.OutputID)
170 | data, err := os.ReadFile(filepath.Join(dir, "trim.txt"))
171 | if err != nil {
172 | t.Fatal(err)
173 | }
174 | checkTime(fmt.Sprintf("%x-a", dummyID(2)), start)
175 |
176 | // Trim less than a day later should not do any work at all.
177 | now = start + 80000
178 | c.Trim()
179 | if _, err := c.Get(id); err != nil {
180 | t.Fatal(err)
181 | }
182 | c.OutputFile(entry.OutputID)
183 | data2, err := os.ReadFile(filepath.Join(dir, "trim.txt"))
184 | if err != nil {
185 | t.Fatal(err)
186 | }
187 | if !bytes.Equal(data, data2) {
188 | t.Fatalf("second trim did work: %q -> %q", data, data2)
189 | }
190 |
191 | // Fast forward and do another trim just before the 5 day cutoff.
192 | // Note that because of usedQuantum the cutoff is actually 5 days + 1 hour.
193 | // We used c.Get(id) just now, so 5 days later it should still be kept.
194 | // On the other hand almost a full day has gone by since we wrote dummyID(2)
195 | // and we haven't looked at it since, so 5 days later it should be gone.
196 | now += 5 * 86400
197 | checkTime(fmt.Sprintf("%x-a", dummyID(2)), start)
198 | c.Trim()
199 | if _, err := c.Get(id); err != nil {
200 | t.Fatal(err)
201 | }
202 | c.OutputFile(entry.OutputID)
203 | mtime3 := now
204 | if _, err := c.Get(dummyID(2)); err == nil { // haven't done a Get for this since original write above
205 | t.Fatalf("Trim did not remove dummyID(2)")
206 | }
207 |
208 | // The c.Get(id) refreshed id's mtime again.
209 | // Check that another 5 days later it is still not gone,
210 | // but check by using checkTime, which doesn't bring mtime forward.
211 | now += 5 * 86400
212 | c.Trim()
213 | checkTime(fmt.Sprintf("%x-a", id), mtime3)
214 | checkTime(fmt.Sprintf("%x-d", entry.OutputID), mtime3)
215 |
216 | // Half a day later Trim should still be a no-op, because there was a Trim recently.
217 | // Even though the entry for id is now old enough to be trimmed,
218 | // it gets a reprieve until the time comes for a new Trim scan.
219 | now += 86400 / 2
220 | c.Trim()
221 | checkTime(fmt.Sprintf("%x-a", id), mtime3)
222 | checkTime(fmt.Sprintf("%x-d", entry.OutputID), mtime3)
223 |
224 | // Another half a day later, Trim should actually run, and it should remove id.
225 | now += 86400/2 + 1
226 | c.Trim()
227 | if _, err := c.Get(dummyID(1)); err == nil {
228 | t.Fatal("Trim did not remove dummyID(1)")
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/src/cache/hash.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Tamás Gulácsi. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package cache
6 |
7 | import (
8 | "crypto/sha256"
9 | "hash"
10 | )
11 |
12 | const HashSize = sha256.Size
13 |
14 | type ID [HashSize]byte
15 | type Hash struct {
16 | hash.Hash
17 | }
18 |
19 | func NewHash() Hash { return Hash{Hash: sha256.New()} }
20 | func (h Hash) SumID() ID {
21 | var a ID
22 | h.Hash.Sum(a[:0])
23 | return a
24 | }
25 |
--------------------------------------------------------------------------------
/src/cli.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "os"
8 | "strings"
9 | "text/tabwriter"
10 |
11 | "github.com/oriser/regroup"
12 |
13 | "github.com/rsteube/carapace"
14 | "github.com/rsteube/carapace/pkg/style"
15 | "github.com/spf13/cobra"
16 |
17 | "github.com/paisano-nix/paisano/data"
18 | "github.com/paisano-nix/paisano/flake"
19 | )
20 |
21 | type Spec struct {
22 | Cell string `regroup:"cell,required"`
23 | Block string `regroup:"block,required"`
24 | Target string `regroup:"target,required"`
25 | Action string `regroup:"action,required"`
26 | }
27 |
28 | var re = regroup.MustCompile(`^//(?P[^/]+)/(?P[^/]+)/(?P.+):(?P[^:]+)`)
29 |
30 | var forSystem string
31 |
32 | var rootCmd = &cobra.Command{
33 | Use: fmt.Sprintf("%[1]s //[cell]/[block]/[target]:[action] [args...]", argv0),
34 | DisableFlagsInUseLine: true,
35 | Version: fmt.Sprintf("%s (%s)", buildVersion, buildCommit),
36 | Short: fmt.Sprintf("%[1]s is the CLI / TUI companion for %[2]s", argv0, project),
37 | Long: fmt.Sprintf(`%[1]s is the CLI / TUI companion for %[2]s.
38 |
39 | - Invoke without any arguments to start the TUI.
40 | - Invoke with a target spec and action to run a known target's action directly.
41 |
42 | Enable autocompletion via '%[1]s _carapace '.
43 | For more instructions, see: https://rsteube.github.io/carapace/carapace/gen/hiddenSubcommand.html
44 | `, argv0, project),
45 | Args: func(cmd *cobra.Command, args []string) error {
46 | s := &Spec{}
47 | if err := re.MatchToTarget(args[0], s); err != nil {
48 | return fmt.Errorf("invalid argument format: %s", args[0])
49 | }
50 | return nil
51 | },
52 | RunE: func(cmd *cobra.Command, args []string) error {
53 | s := &Spec{}
54 | if err := re.MatchToTarget(args[0], s); err != nil {
55 | return err
56 | }
57 | command := flake.RunActionCmd{
58 | ShowCmdStr: false,
59 | CmdStr: strings.Join(args[:], " "),
60 | System: forSystem,
61 | Cell: s.Cell,
62 | Block: s.Block,
63 | Target: s.Target,
64 | Action: s.Action}
65 | if err := command.Exec(args[1:]); err != nil {
66 | return err
67 | }
68 | return nil
69 |
70 | },
71 | }
72 | var reCacheCmd = &cobra.Command{
73 | Use: "re-cache",
74 | Short: "Refresh the CLI cache.",
75 | Long: `Refresh the CLI cache.
76 | Use this command to cold-start or refresh the CLI cache.
77 | The TUI does this automatically, but the command completion needs manual initialization of the CLI cache.`,
78 | Args: cobra.NoArgs,
79 | RunE: func(cmd *cobra.Command, args []string) error {
80 | c, key, loadCmd, buf, err := flake.LoadFlakeCmd()
81 | if err != nil {
82 | return fmt.Errorf("while loading flake (cmd '%v'): %w", loadCmd, err)
83 | }
84 | loadCmd.Run()
85 | c.PutBytes(*key, buf.Bytes())
86 | return nil
87 | },
88 | }
89 | var checkCmd = &cobra.Command{
90 | Use: "check",
91 | Short: "Validate the repository.",
92 | Long: fmt.Sprintf(`Validates that the repository conforms to %[1]s.
93 | Returns a non-zero exit code and an error message if the repository is not a valid %[1]s repository.
94 | The TUI does this automatically.`, project),
95 | Args: cobra.NoArgs,
96 | RunE: func(cmd *cobra.Command, args []string) error {
97 | _, _, loadCmd, _, err := flake.LoadFlakeCmd()
98 | loadCmd.Args = append(loadCmd.Args, "--trace-verbose")
99 | if err != nil {
100 | return fmt.Errorf("while loading flake (cmd '%v'): %w", loadCmd, err)
101 | }
102 | loadCmd.Stderr = os.Stderr
103 | if err := loadCmd.Run(); err != nil {
104 | os.Exit(1)
105 | }
106 | fmt.Printf("Valid %s repository ✓\n", project)
107 |
108 | return nil
109 | },
110 | }
111 | var listCmd = &cobra.Command{
112 | Use: "list",
113 | Short: "List available targets.",
114 | Long: `List available targets.
115 | Shows a list of all available targets. Can be used as an alternative to the TUI.
116 | Also loads the CLI cache, if no cache is found. Reads the cache, otherwise.`,
117 | Args: cobra.NoArgs,
118 | RunE: func(cmd *cobra.Command, args []string) error {
119 | cache, key, loadCmd, buf, err := flake.LoadFlakeCmd()
120 | if err != nil {
121 | return fmt.Errorf("while loading flake (cmd '%v'): %w", loadCmd, err)
122 | }
123 | cached, _, err := cache.GetBytes(*key)
124 | var root *data.Root
125 | if err == nil {
126 | root, err = LoadJson(bytes.NewReader(cached))
127 | if err != nil {
128 | return fmt.Errorf("while loading cached json: %w", err)
129 | }
130 | } else {
131 | loadCmd.Run()
132 | bufA := &bytes.Buffer{}
133 | r := io.TeeReader(buf, bufA)
134 | root, err = LoadJson(r)
135 | if err != nil {
136 | return fmt.Errorf("while loading json (cmd: '%v'): %w", loadCmd, err)
137 | }
138 | cache.PutBytes(*key, bufA.Bytes())
139 | }
140 | w := tabwriter.NewWriter(os.Stdout, 5, 2, 4, ' ', 0)
141 | for _, c := range root.Cells {
142 | for _, o := range c.Blocks {
143 | for _, t := range o.Targets {
144 | for _, a := range t.Actions {
145 | fmt.Fprintf(w, "//%s/%s/%s:%s\t--\t%s: %s\n", c.Name, o.Name, t.Name, a.Name, t.Description(), a.Description())
146 | }
147 | }
148 | }
149 | }
150 | w.Flush()
151 | return nil
152 | },
153 | }
154 |
155 | func ExecuteCli() {
156 | if err := rootCmd.Execute(); err != nil {
157 | fmt.Println(err)
158 | os.Exit(1)
159 | }
160 | }
161 |
162 | func init() {
163 | rootCmd.Flags().StringVar(&forSystem, "for", "", "system, for which the target will be built (e.g. 'x86_64-linux')")
164 | rootCmd.AddCommand(reCacheCmd)
165 | rootCmd.AddCommand(listCmd)
166 | rootCmd.AddCommand(checkCmd)
167 | carapace.Gen(rootCmd).Standalone()
168 | // completes: '//cell/block/target:action'
169 | carapace.Gen(rootCmd).PositionalCompletion(
170 | carapace.ActionCallback(func(ctx carapace.Context) carapace.Action {
171 | cache, key, _, _, err := flake.LoadFlakeCmd()
172 | if err != nil {
173 | return carapace.ActionMessage(fmt.Sprintf("%v\n", err))
174 | }
175 | cached, _, err := cache.GetBytes(*key)
176 | var root *data.Root
177 | if err == nil {
178 | root, err = LoadJson(bytes.NewReader(cached))
179 | if err != nil {
180 | return carapace.ActionMessage(fmt.Sprintf("%v\n", err))
181 | }
182 | } else {
183 | return carapace.ActionMessage(fmt.Sprintf("No completion cache: please initialize by running '%[1]s re-cache'.", argv0))
184 | }
185 | var cells = []string{}
186 | var blocks = map[string][]string{}
187 | var targets = map[string]map[string][]string{}
188 | var actions = map[string]map[string]map[string][]string{}
189 | for _, c := range root.Cells {
190 | blocks[c.Name] = []string{}
191 | targets[c.Name] = map[string][]string{}
192 | actions[c.Name] = map[string]map[string][]string{}
193 | cells = append(cells, c.Name, "cell")
194 | for _, b := range c.Blocks {
195 | targets[c.Name][b.Name] = []string{}
196 | actions[c.Name][b.Name] = map[string][]string{}
197 | blocks[c.Name] = append(blocks[c.Name], b.Name, "block")
198 | for _, t := range b.Targets {
199 | actions[c.Name][b.Name][t.Name] = []string{}
200 | targets[c.Name][b.Name] = append(targets[c.Name][b.Name], t.Name, t.Description())
201 | for _, a := range t.Actions {
202 | actions[c.Name][b.Name][t.Name] = append(
203 | actions[c.Name][b.Name][t.Name],
204 | a.Name,
205 | a.Description(),
206 | )
207 | }
208 | }
209 | }
210 | }
211 | return carapace.ActionMultiParts("/", func(c carapace.Context) carapace.Action {
212 | switch len(c.Parts) {
213 | // start with ; no typing
214 | case 0:
215 | return carapace.ActionValuesDescribed(
216 | cells...,
217 | ).Invoke(c).Prefix("//").Suffix("/").ToA().Style(
218 | style.Of(style.Bold, style.Carapace.Highlight(1)))
219 | // only a single / typed
220 | case 1:
221 | return carapace.ActionValuesDescribed(
222 | cells...,
223 | ).Invoke(c).Prefix("/").Suffix("/").ToA()
224 | // start typing cell
225 | case 2:
226 | return carapace.ActionValuesDescribed(
227 | cells...,
228 | ).Invoke(c).Suffix("/").ToA().Style(
229 | style.Carapace.Highlight(1))
230 | // start typing block
231 | case 3:
232 | return carapace.ActionValuesDescribed(
233 | blocks[c.Parts[2]]...,
234 | ).Invoke(c).Suffix("/").ToA().Style(
235 | style.Carapace.Highlight(2))
236 | // start typing target
237 | case 4:
238 | return carapace.ActionMultiParts(":", func(d carapace.Context) carapace.Action {
239 | switch len(d.Parts) {
240 | // start typing target
241 | case 0:
242 | return carapace.ActionValuesDescribed(
243 | targets[c.Parts[2]][c.Parts[3]]...,
244 | ).Invoke(c).Suffix(":").ToA().Style(
245 | style.Carapace.Highlight(3))
246 | // start typing action
247 | case 1:
248 | return carapace.ActionValuesDescribed(
249 | actions[c.Parts[2]][c.Parts[3]][d.Parts[0]]...,
250 | ).Invoke(c).ToA()
251 | default:
252 | return carapace.ActionValues()
253 | }
254 | })
255 | default:
256 | return carapace.ActionValues()
257 | }
258 | })
259 |
260 | }),
261 | )
262 | }
263 |
--------------------------------------------------------------------------------
/src/data/data.go:
--------------------------------------------------------------------------------
1 | package data
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/paisano-nix/paisano/flake"
7 | )
8 |
9 | var (
10 | targetTemplate = "//%s/%s/%s"
11 | actionTemplate = "//%s/%s/%s:%s"
12 | noReadme = "🥺 No Readme available ...\n\n💡 But hey! You could create one ...\n\n💪 Start with: `$EDITOR %s` (don't forget to commit it!)\n\n👉 It will also be rendered in the docs!"
13 | noDescription = "🥺 Target has no 'meta.description' attribute"
14 | )
15 |
16 | type Root struct {
17 | Cells []Cell
18 | }
19 |
20 | type Cell struct {
21 | Name string `json:"cell"`
22 | Readme *string `json:"readme,omitempty"`
23 | Blocks []Block `json:"cellBlocks"`
24 | }
25 |
26 | type Block struct {
27 | Name string `json:"cellBlock"`
28 | Readme *string `json:"readme,omitempty"`
29 | Blocktype string `json:"blockType"`
30 | Targets []Target `json:"targets"`
31 | }
32 |
33 | type Action struct {
34 | Name string `json:"name"`
35 | Descr string `json:"description"`
36 | RequiresArgs *bool `json:"requiresArgs,omitempty"`
37 | }
38 |
39 | func (a Action) Title() string { return a.Name }
40 | func (a Action) Description() string { return a.Descr }
41 | func (a Action) FilterValue() string { return a.Title() }
42 |
43 | type Target struct {
44 | Name string `json:"name"`
45 | Readme *string `json:"readme,omitempty"`
46 | Deps []string `json:"deps"`
47 | Descr *string `json:"description,omitempty"`
48 | Actions []Action `json:"actions"`
49 | }
50 |
51 | func (t Target) Description() string {
52 | if t.Descr != nil {
53 | return "💡 " + *t.Descr
54 | } else {
55 | return noDescription
56 | }
57 | }
58 |
59 | func (r *Root) Select(ci, bi, ti int) (Cell, Block, Target) {
60 | var (
61 | c = r.Cells[ci]
62 | b = c.Blocks[bi]
63 | t = b.Targets[ti]
64 | )
65 | return c, b, t
66 | }
67 |
68 | func (r *Root) ActionArg(ci, bi, ti, ai int) string {
69 | c, b, t := r.Select(ci, bi, ti)
70 | a := t.Actions[ai]
71 | return fmt.Sprintf(actionTemplate, c.Name, b.Name, t.Name, a.Name)
72 | }
73 |
74 | func (r *Root) ActionTitle(ci, bi, ti, ai int) string {
75 | _, _, t := r.Select(ci, bi, ti)
76 | a := t.Actions[ai]
77 | return a.Title()
78 | }
79 |
80 | func (r *Root) ActionDescription(ci, bi, ti, ai int) string {
81 | _, _, t := r.Select(ci, bi, ti)
82 | a := t.Actions[ai]
83 | return a.Description()
84 | }
85 | func (r *Root) ActionRequiresArgs(ci, bi, ti, ai int) *bool {
86 | _, _, t := r.Select(ci, bi, ti)
87 | a := t.Actions[ai]
88 | return a.RequiresArgs
89 | }
90 |
91 | func (r *Root) TargetTitle(ci, bi, ti int) string {
92 | c, b, t := r.Select(ci, bi, ti)
93 | return fmt.Sprintf(targetTemplate, c.Name, b.Name, t.Name)
94 | }
95 |
96 | func (r *Root) TargetDescription(ci, bi, ti int) string {
97 | _, _, t := r.Select(ci, bi, ti)
98 | return t.Description()
99 | }
100 | func (r *Root) Cell(ci, bi, ti int) Cell { c, _, _ := r.Select(ci, bi, ti); return c }
101 | func (r *Root) CellName(ci, bi, ti int) string { return r.Cell(ci, bi, ti).Name }
102 | func (r *Root) CellHelp(ci, bi, ti int) string {
103 | if r.HasCellHelp(ci, bi, ti) {
104 | return *r.Cell(ci, bi, ti).Readme
105 | } else {
106 | return fmt.Sprintf(noReadme, fmt.Sprintf("%s/%s/Readme.md", flake.CellsFrom.Value(), r.CellName(ci, bi, ti)))
107 | }
108 | }
109 | func (r *Root) HasCellHelp(ci, bi, ti int) bool {
110 | c := r.Cell(ci, bi, ti)
111 | return c.Readme != nil
112 | }
113 | func (r *Root) Block(ci, bi, ti int) Block { _, o, _ := r.Select(ci, bi, ti); return o }
114 | func (r *Root) BlockName(ci, bi, ti int) string { return r.Block(ci, bi, ti).Name }
115 | func (r *Root) BlockHelp(ci, bi, ti int) string {
116 | if r.HasBlockHelp(ci, bi, ti) {
117 | return *r.Block(ci, bi, ti).Readme
118 | } else {
119 | return fmt.Sprintf(noReadme, fmt.Sprintf("%s/%s/%s/Readme.md", flake.CellsFrom.Value(), r.CellName(ci, bi, ti), r.BlockName(ci, bi, ti)))
120 | }
121 | }
122 | func (r *Root) HasBlockHelp(ci, bi, ti int) bool {
123 | b := r.Block(ci, bi, ti)
124 | return b.Readme != nil
125 | }
126 | func (r *Root) Target(ci, bi, ti int) Target { _, _, t := r.Select(ci, bi, ti); return t }
127 | func (r *Root) TargetName(ci, bi, ti int) string { return r.Target(ci, bi, ti).Name }
128 | func (r *Root) TargetHelp(ci, bi, ti int) string {
129 | if r.HasTargetHelp(ci, bi, ti) {
130 | return *r.Target(ci, bi, ti).Readme
131 | } else {
132 | return fmt.Sprintf(noReadme, fmt.Sprintf("%s/%s/%s/%s.md", flake.CellsFrom.Value(), r.CellName(ci, bi, ti), r.BlockName(ci, bi, ti), r.TargetName(ci, bi, ti)))
133 | }
134 | }
135 | func (r *Root) HasTargetHelp(ci, bi, ti int) bool {
136 | t := r.Target(ci, bi, ti)
137 | return t.Readme != nil
138 | }
139 |
140 | func (r *Root) Len() int {
141 | sum := 0
142 | for _, c := range r.Cells {
143 | for _, o := range c.Blocks {
144 | sum += len(o.Targets)
145 | }
146 | }
147 | return sum
148 | }
149 |
--------------------------------------------------------------------------------
/src/default.md:
--------------------------------------------------------------------------------
1 | The CLI/TUI only has a single target, called `default`.
2 |
3 | Please consult the Cell Block's Readme for more information.
4 |
--------------------------------------------------------------------------------
/src/env/env.go:
--------------------------------------------------------------------------------
1 | package env
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "strings"
7 |
8 | spec "github.com/numtide/prj-spec/contrib/go"
9 | )
10 |
11 | // extraNixConfig implements quality of life flags for the nix command invocation
12 | var extraNixConfig = strings.Join([]string{
13 | // can never occur: actions invoke store path copies of the flake
14 | // "warn-dirty = false",
15 | "accept-flake-config = true",
16 | "builders-use-substitutes = true",
17 | // TODO: these are unfortunately not available for setting as env flags
18 | // update-lock-file = false,
19 | // write-lock-file = false,
20 | }, "\n")
21 |
22 | func SetEnv() {
23 | spec.SetAll() // PRJ_*
24 |
25 | nixConfigEnv, present := os.LookupEnv("NIX_CONFIG")
26 | if !present {
27 | os.Setenv("NIX_CONFIG", extraNixConfig)
28 | } else {
29 | os.Setenv("NIX_CONFIG", fmt.Sprintf("%s\n%s", nixConfigEnv, extraNixConfig))
30 | }
31 | }
32 |
33 | func GetStateActionPath() (string, error) { return spec.DataFile("last-action") }
34 | func GetProjectMetadataCacheDir() (string, error) {
35 | path, err := spec.CacheFile("metadata")
36 | if err != nil {
37 | return "", err
38 | }
39 | err = os.MkdirAll(path, os.ModePerm)
40 | if err != nil {
41 | return "", err
42 | }
43 | return path, nil
44 | }
45 |
--------------------------------------------------------------------------------
/src/flake/flake.go:
--------------------------------------------------------------------------------
1 | package flake
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "fmt"
7 | "os/exec"
8 | "text/template"
9 |
10 | "github.com/hymkor/go-lazy"
11 | )
12 |
13 | var (
14 | registry = "__std" // keep for now for historic reasons
15 | flakeRegistry = func(flake string) string { return fmt.Sprintf("%[2]s#%[1]s", registry, flake) }
16 | )
17 |
18 | type outt struct {
19 | drvPath string `json:"drvPath"`
20 | outputs map[string]string `json:"outputs"`
21 | }
22 |
23 | var CellsFrom = lazy.Of[string]{
24 | New: func() string {
25 | if s, err := getCellsFrom(); err != nil {
26 | return "${cellsFrom}"
27 | } else {
28 | return s
29 | }
30 | },
31 | }
32 |
33 | // tprintf passed template string is formatted usign its operands and returns the resulting string.
34 | // Spaces are added between operands when neither is a string.
35 | func tprintf(data interface{}, tmpl string) string {
36 | t := template.Must(template.New("tmp").Parse(tmpl))
37 | buf := &bytes.Buffer{}
38 | if err := t.Execute(buf, data); err != nil {
39 | return ""
40 | }
41 | return buf.String()
42 | }
43 |
44 | func getNix() (string, error) {
45 | nix, err := exec.LookPath("nix")
46 | if err != nil {
47 | return "", errors.New("You need to install 'nix' in order to use this tool")
48 | }
49 | return nix, nil
50 | }
51 |
52 | func getCurrentSystem() (string, error) {
53 | // detect the current system
54 | nix, err := getNix()
55 | if err != nil {
56 | return "", err
57 | }
58 | currentSystem, err := exec.Command(
59 | nix, "eval", "--raw", "--impure", "--expr", "builtins.currentSystem",
60 | ).Output()
61 | if err != nil {
62 | if exitErr, ok := err.(*exec.ExitError); ok {
63 | return "", fmt.Errorf("%w, stderr:\n%s", exitErr, exitErr.Stderr)
64 | }
65 | return "", err
66 | }
67 | currentSystemStr := string(currentSystem)
68 | return currentSystemStr, nil
69 | }
70 |
71 | func getCellsFrom() (string, error) {
72 | nix, err := getNix()
73 | if err != nil {
74 | return "", err
75 | }
76 | cellsFrom, err := exec.Command(
77 | nix, "eval", "--raw", flakeRegistry(".")+".cellsFrom",
78 | ).Output()
79 | if err != nil {
80 | if exitErr, ok := err.(*exec.ExitError); ok {
81 | return "", fmt.Errorf("%w, stderr:\n%s", exitErr, exitErr.Stderr)
82 | }
83 | return "", err
84 | }
85 | return string(cellsFrom[:]), nil
86 | }
87 |
--------------------------------------------------------------------------------
/src/flake/load.go:
--------------------------------------------------------------------------------
1 | package flake
2 |
3 | import (
4 | "bytes"
5 | "os"
6 | "os/exec"
7 | "strings"
8 |
9 | "github.com/paisano-nix/paisano/cache"
10 | "github.com/paisano-nix/paisano/env"
11 | )
12 |
13 | func LoadFlakeCmd() (*cache.Cache, *cache.ActionID, *exec.Cmd, *bytes.Buffer, error) {
14 |
15 | nix, err := getNix()
16 | if err != nil {
17 | return nil, nil, nil, nil, err
18 | }
19 |
20 | currentSystem, err := getCurrentSystem()
21 | if err != nil {
22 | return nil, nil, nil, nil, err
23 | }
24 |
25 | devNull, err := os.Open(os.DevNull)
26 | if err != nil {
27 | return nil, nil, nil, nil, err
28 | }
29 |
30 | // load the paisano metadata from the flake
31 | buf := new(bytes.Buffer)
32 | args := []string{
33 | "eval",
34 | "--json",
35 | "--no-update-lock-file",
36 | "--no-write-lock-file",
37 | "--no-warn-dirty",
38 | "--accept-flake-config",
39 | flakeRegistry(".") + ".init." + currentSystem}
40 | cmd := exec.Command(nix, args...)
41 | cmd.Stdin = devNull
42 | cmd.Stdout = buf
43 |
44 | // initialize cache
45 | metadataCacheDir, err := env.GetProjectMetadataCacheDir()
46 | if err != nil {
47 | return nil, nil, nil, nil, err
48 | }
49 | c, err := cache.Open(metadataCacheDir)
50 | if err != nil {
51 | return nil, nil, nil, nil, err
52 | }
53 | key := cache.NewActionID([]byte(strings.Join(args, "")))
54 |
55 | return c, &key, cmd, buf, nil
56 | }
57 |
--------------------------------------------------------------------------------
/src/flake/run.go:
--------------------------------------------------------------------------------
1 | package flake
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "os"
7 | "os/exec"
8 | "strings"
9 | "syscall"
10 |
11 | "github.com/paisano-nix/paisano/env"
12 | )
13 |
14 | type RunActionCmd struct {
15 | ShowCmdStr bool
16 | CmdStr string
17 | System string
18 | Cell string
19 | Block string
20 | Target string
21 | Action string
22 | RequiresArgs *bool
23 | }
24 |
25 | func (c *RunActionCmd) Assemble(extraArgs []string) (string, []string, error) {
26 | nix, err := getNix()
27 | if err != nil {
28 | return "", nil, err
29 | }
30 |
31 | currentSystem, err := getCurrentSystem()
32 | if err != nil {
33 | return "", nil, err
34 | }
35 |
36 | args, err := c.getArgs(currentSystem)
37 | if err != nil {
38 | return "", nil, err
39 | }
40 |
41 | if extraArgs != nil && len(extraArgs) > 0 {
42 | args = append(args, "--")
43 | args = append(args, extraArgs...)
44 | }
45 | return nix, args, nil
46 | }
47 |
48 | func (c *RunActionCmd) Build(nix string, args, extraArgs []string) (string, []string, error) {
49 | if c.RequiresArgs != nil && *c.RequiresArgs == true && len(extraArgs) == 0 {
50 | return "", nil, errors.New(c.CmdStr + " - requires on or more arguments; run from command line")
51 | }
52 | bash, err := exec.LookPath("bash")
53 | if err != nil {
54 | return "", nil, err
55 | }
56 | // grep, err := exec.LookPath("grep")
57 | // if err != nil {
58 | // return "", nil, err
59 | // }
60 | nom, err := exec.LookPath("nom")
61 | if err == nil {
62 | nix = nom
63 | }
64 | actionPath, err := env.GetStateActionPath()
65 | if err != nil {
66 | return "", nil, err
67 | }
68 | printout := ""
69 | if c.ShowCmdStr {
70 | printout += "echo -e \"\x1b[1;37m------------" + strings.Repeat("-", len(c.CmdStr)) + "-\x1b[0m\";"
71 | printout += "echo -e \"\x1b[1;37m Executing: \x1b[1;32m" + c.CmdStr + "\x1b[0m\";"
72 | printout += "echo -e \"\x1b[1;37m------------" + strings.Repeat("-", len(c.CmdStr)) + "-\x1b[0m\";"
73 | }
74 | args = append(args, "--out-link", actionPath)
75 | args = append(args,
76 | "--no-update-lock-file",
77 | "--no-write-lock-file",
78 | "--no-warn-dirty",
79 | "--accept-flake-config",
80 | "--builders-use-substitutes",
81 | "|| exit 1;",
82 | printout,
83 | "exec", actionPath, "\"${@}\"",
84 | )
85 | cmd := []string{bash, "-c", nix + " build " + strings.Join(args, " ")}
86 | if extraArgs != nil && len(extraArgs) > 0 {
87 | cmd = append(cmd, "--")
88 | cmd = append(cmd, extraArgs...)
89 | }
90 | // fmt.Printf("%+v\n", cmd)
91 | return bash, cmd, nil
92 | }
93 |
94 | func (c *RunActionCmd) Exec(extraArgs []string) error {
95 |
96 | nix, args, err := c.Assemble(nil)
97 | if err != nil {
98 | return err
99 | }
100 |
101 | bash, cmd, err := c.Build(nix, args, extraArgs)
102 | if err != nil {
103 | return err
104 | }
105 |
106 | env.SetEnv() // PRJ_* + NIX_CONFIG
107 | if err := syscall.Exec(bash, cmd, os.Environ()); err != nil {
108 | return err
109 | }
110 | return nil
111 | }
112 |
113 | func (c *RunActionCmd) getArgs(currentSystem string) ([]string, error) {
114 |
115 | if c.System == currentSystem {
116 | return nil, fmt.Errorf("set the --for flag to a different system than the current one ('%s')", currentSystem)
117 | }
118 |
119 | if c.System != "" {
120 | // if system is set, the impure flag provides a "hack" so that we
121 | // can transport this information to the action evaluation without
122 | // incurring in a prohibitively complex (m*n) data structure in
123 | // which we would have to account for _all_ combinations of current
124 | // and build system
125 | return []string{"--impure", c.renderFragmentFor(c.System)}, nil
126 | }
127 | return []string{c.renderFragmentFor(currentSystem)}, nil
128 | }
129 |
130 | func (c *RunActionCmd) renderFragmentFor(system string) string {
131 | return tprintf(c, "'"+flakeRegistry(".")+".actions."+system+".\"{{.Cell}}\".\"{{.Block}}\".\"{{.Target}}\".\"{{.Action}}\"'")
132 | }
133 |
--------------------------------------------------------------------------------
/src/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/paisano-nix/paisano
2 |
3 | go 1.21
4 |
5 | toolchain go1.21.6
6 |
7 | require (
8 | github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2
9 | github.com/aymanbagabas/go-osc52 v1.2.2
10 | github.com/charmbracelet/bubbles v0.18.0
11 | github.com/charmbracelet/bubbletea v0.25.0
12 | github.com/charmbracelet/lipgloss v0.9.1
13 | github.com/google/renameio/v2 v2.0.0
14 | github.com/hymkor/go-lazy v0.4.0
15 | github.com/knipferrc/teacup v0.3.1
16 | github.com/numtide/prj-spec/contrib/go v0.0.0-00010101000000-000000000000
17 | github.com/oriser/regroup v0.0.0-20230527212431-1b00c9bdbc5b
18 | github.com/rogpeppe/go-internal v1.12.0
19 | github.com/rsteube/carapace v0.50.0
20 | github.com/spf13/cobra v1.8.0
21 | )
22 |
23 | require (
24 | github.com/adrg/xdg v0.4.0 // indirect
25 | github.com/alecthomas/chroma v0.10.0 // indirect
26 | github.com/atotto/clipboard v0.1.4 // indirect
27 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
28 | github.com/aymerick/douceur v0.2.0 // indirect
29 | github.com/charmbracelet/glamour v0.6.0 // indirect
30 | github.com/containerd/console v1.0.4 // indirect
31 | github.com/dlclark/regexp2 v1.10.0 // indirect
32 | github.com/fatih/color v1.16.0 // indirect
33 | github.com/gorilla/css v1.0.1 // indirect
34 | github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f // indirect
35 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
36 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
37 | github.com/mattn/go-colorable v0.1.13 // indirect
38 | github.com/mattn/go-isatty v0.0.20 // indirect
39 | github.com/mattn/go-localereader v0.0.1 // indirect
40 | github.com/mattn/go-runewidth v0.0.15 // indirect
41 | github.com/microcosm-cc/bluemonday v1.0.26 // indirect
42 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
43 | github.com/muesli/cancelreader v0.2.2 // indirect
44 | github.com/muesli/reflow v0.3.0 // indirect
45 | github.com/muesli/termenv v0.15.2 // indirect
46 | github.com/olekukonko/tablewriter v0.0.5 // indirect
47 | github.com/rivo/uniseg v0.4.7 // indirect
48 | github.com/rsteube/carapace-shlex v0.1.2 // indirect
49 | github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
50 | github.com/spf13/pflag v1.0.5 // indirect
51 | github.com/yuin/goldmark v1.7.0 // indirect
52 | github.com/yuin/goldmark-emoji v1.0.2 // indirect
53 | golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect
54 | golang.org/x/net v0.21.0 // indirect
55 | golang.org/x/sync v0.6.0 // indirect
56 | golang.org/x/sys v0.17.0 // indirect
57 | golang.org/x/term v0.17.0 // indirect
58 | golang.org/x/text v0.14.0 // indirect
59 | gopkg.in/yaml.v3 v3.0.1 // indirect
60 | )
61 |
62 | replace github.com/numtide/prj-spec/contrib/go => github.com/blaggacao/prj-spec/contrib/go v0.0.0-20240217212339-2ca723041d63
63 |
--------------------------------------------------------------------------------
/src/go.sum:
--------------------------------------------------------------------------------
1 | github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4=
2 | github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w=
3 | github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
4 | github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
5 | github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
6 | github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
7 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
8 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
9 | github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
10 | github.com/aymanbagabas/go-osc52 v1.2.2 h1:NT7wkhEhPTcKnBCdPi9djmyy9L3JOL4+3SsfJyqptCo=
11 | github.com/aymanbagabas/go-osc52 v1.2.2/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
12 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
13 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
14 | github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
15 | github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
16 | github.com/blaggacao/prj-spec/contrib/go v0.0.0-20240217212339-2ca723041d63 h1:Xt5Myl9RJmgTVbzq6DAWSbr+r8MAodb88fhCUVC9uDs=
17 | github.com/blaggacao/prj-spec/contrib/go v0.0.0-20240217212339-2ca723041d63/go.mod h1:Z8M0Lsc1sBOwnBHfy8nrFNuO9yqkE4+8IoPpbdoPHo4=
18 | github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
19 | github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
20 | github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
21 | github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
22 | github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc=
23 | github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc=
24 | github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
25 | github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
26 | github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
27 | github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
28 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
29 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
30 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
31 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
32 | github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
33 | github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
34 | github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
35 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
36 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
37 | github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
38 | github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
39 | github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
40 | github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
41 | github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
42 | github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8=
43 | github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI=
44 | github.com/hymkor/go-lazy v0.4.0 h1:KHZzn64U0UTwdj7rn6kp7RMgFQPQ7rp2ld5H3d2a+JI=
45 | github.com/hymkor/go-lazy v0.4.0/go.mod h1:7weoQ6ibzJeNdZ6sj50tjiCv0bJdQeXXXo2EMGm8tH4=
46 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
47 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
48 | github.com/knipferrc/teacup v0.3.1 h1:EhEGAONKkOgW/7R339jYa+En03UaDlGSY/AImncMfBo=
49 | github.com/knipferrc/teacup v0.3.1/go.mod h1:96Y/xb6KS3vOaOxJuZZwnFas+4m7/FKeQtllLikkjl0=
50 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
51 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
52 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
53 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
54 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
55 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
56 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
57 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
58 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
59 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
60 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
61 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
62 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
63 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
64 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
65 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
66 | github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
67 | github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
68 | github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
69 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
70 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
71 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
72 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
73 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
74 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
75 | github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
76 | github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
77 | github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
78 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
79 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
80 | github.com/oriser/regroup v0.0.0-20230527212431-1b00c9bdbc5b h1:9L56kn3D7E9jd2R9U9p7tfzaBaLTVPt4HgnrP+g2VGk=
81 | github.com/oriser/regroup v0.0.0-20230527212431-1b00c9bdbc5b/go.mod h1:6eb1+OYHjOvThrtgEVue70NTfmzkalZgohRtndAUUbI=
82 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
83 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
84 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
85 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
86 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
87 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
88 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
89 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
90 | github.com/rsteube/carapace v0.50.0 h1:LO3ehEjcdbIx9owiyCiVfgL5A0cJPGq4X7c8V5DsA20=
91 | github.com/rsteube/carapace v0.50.0/go.mod h1:syVOvI8e2rEEK/9aMZxfWuHvcnQK/EcnTV4roClEnLE=
92 | github.com/rsteube/carapace-shlex v0.1.2 h1:ZKjhIfXoCkEnzensMglTaLbkNOaLkmM8SCRshpJKx6s=
93 | github.com/rsteube/carapace-shlex v0.1.2/go.mod h1:zPw1dOFwvLPKStUy9g2BYKanI6bsQMATzDMYQQybo3o=
94 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
95 | github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y=
96 | github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
97 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
98 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
99 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
100 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
101 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
102 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
103 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
104 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
105 | github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
106 | github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
107 | github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
108 | github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
109 | github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
110 | github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
111 | github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
112 | golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
113 | golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
114 | golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
115 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
116 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
117 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
118 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
119 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
120 | golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
121 | golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
122 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
123 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
124 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
125 | golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
126 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
127 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
128 | golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
129 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
130 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
131 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
132 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
133 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
134 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
135 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
136 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
137 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
138 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
139 |
--------------------------------------------------------------------------------
/src/keys/keys.go:
--------------------------------------------------------------------------------
1 | package keys
2 |
3 | import (
4 | "github.com/charmbracelet/bubbles/key"
5 | "github.com/charmbracelet/bubbles/list"
6 | "github.com/charmbracelet/bubbles/viewport"
7 | )
8 |
9 | const spacebar = " "
10 |
11 | var (
12 | cursorUp = key.NewBinding(key.WithKeys("k", "up"), key.WithHelp("k/↑", "up"))
13 | cursorDown = key.NewBinding(key.WithKeys("j", "down"), key.WithHelp("j/↓", "down"))
14 | cursorLeft = key.NewBinding(key.WithKeys("left"), key.WithHelp("←", "½ back"))
15 | cursorRight = key.NewBinding(key.WithKeys("right"), key.WithHelp("→", "½ forward"))
16 | pageUp = key.NewBinding(key.WithKeys("pgup"), key.WithHelp("pgup", "1 back"))
17 | pageDown = key.NewBinding(key.WithKeys("pgdown", spacebar), key.WithHelp("pgdn", "1 forward"))
18 | home = key.NewBinding(key.WithKeys("home"), key.WithHelp("home", "go to start"))
19 | end = key.NewBinding(key.WithKeys("end"), key.WithHelp("end", "go to end"))
20 | enter = key.NewBinding(key.WithKeys("enter"), key.WithHelp("⏎", "execute"))
21 | textcopy = key.NewBinding(key.WithKeys("c", "ctrl+c"), key.WithHelp("c", "copy cmd"))
22 | search = key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "filter"))
23 | showReadme = key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "inspect"))
24 | closeReadme = key.NewBinding(key.WithKeys("?", "esc"), key.WithHelp("?", "close"))
25 | quit = key.NewBinding(key.WithKeys("q"), key.WithHelp("q", "quit"))
26 | forceQuit = key.NewBinding(key.WithKeys("ctrl+c"))
27 | toggleFocus = key.NewBinding(key.WithKeys("tab", "shift+tab"), key.WithHelp("⇥", "toggle focus"))
28 | cycleTab = key.NewBinding(key.WithKeys("tab"), key.WithHelp("⇥", "cycle tabs"))
29 | reverseCycleTab = key.NewBinding(key.WithKeys("shift+tab"))
30 | )
31 |
32 | type AppKeyMap struct {
33 | ToggleFocus key.Binding
34 | FocusLeft key.Binding
35 | FocusRight key.Binding
36 | ShowReadme key.Binding
37 | Quit key.Binding
38 | ForceQuit key.Binding
39 | }
40 |
41 | func NewAppKeyMap() *AppKeyMap {
42 | return &AppKeyMap{
43 | ToggleFocus: toggleFocus,
44 | FocusLeft: cursorLeft,
45 | FocusRight: cursorRight,
46 | ShowReadme: showReadme,
47 | ForceQuit: forceQuit,
48 | Quit: quit,
49 | }
50 | }
51 |
52 | type ReadmeKeyMap struct {
53 | viewport.KeyMap
54 | CloseReadme key.Binding
55 | CycleTab key.Binding
56 | ReverseCycleTab key.Binding
57 | }
58 |
59 | func NewReadmeKeyMap() *ReadmeKeyMap {
60 | m := &ReadmeKeyMap{
61 | CloseReadme: closeReadme,
62 | CycleTab: cycleTab,
63 | ReverseCycleTab: reverseCycleTab,
64 | }
65 | m.PageDown = pageUp
66 | m.PageUp = pageDown
67 | m.HalfPageUp = cursorLeft
68 | m.HalfPageDown = cursorRight
69 | m.Up = cursorUp
70 | m.Down = cursorDown
71 | return m
72 | }
73 |
74 | // DefaultListKeyMap returns a default set of keybindings.
75 | func DefaultListKeyMap() list.KeyMap {
76 | return list.KeyMap{
77 | // Browsing.
78 | CursorUp: cursorUp,
79 | CursorDown: cursorDown,
80 | PrevPage: pageUp,
81 | NextPage: pageDown,
82 | GoToStart: home,
83 | GoToEnd: end,
84 | Filter: search,
85 |
86 | // Filtering.
87 | ClearFilter: key.NewBinding(
88 | key.WithKeys("esc"),
89 | key.WithHelp("esc", "clear filter"),
90 | ),
91 | CancelWhileFiltering: key.NewBinding(
92 | key.WithKeys("esc"),
93 | key.WithHelp("esc", "cancel"),
94 | ),
95 | AcceptWhileFiltering: key.NewBinding(
96 | key.WithKeys("enter", "tab", "up", "down"),
97 | key.WithHelp("enter", "apply filter"),
98 | ),
99 | }
100 | }
101 |
102 | type ActionDelegateKeyMap struct {
103 | Exec key.Binding
104 | Copy key.Binding
105 | Inspect key.Binding
106 | QuitInspect key.Binding
107 | }
108 |
109 | // Additional short help entries. This satisfies the help.KeyMap interface and
110 | // is entirely optional.
111 | func (d ActionDelegateKeyMap) ShortHelp() []key.Binding {
112 | return []key.Binding{
113 | d.Exec,
114 | d.Copy,
115 | d.Inspect,
116 | d.QuitInspect,
117 | }
118 | }
119 |
120 | func NewActionDelegateKeyMap() *ActionDelegateKeyMap {
121 | return &ActionDelegateKeyMap{
122 | Exec: enter,
123 | Copy: textcopy,
124 | Inspect: showReadme,
125 | QuitInspect: closeReadme,
126 | }
127 | }
128 |
129 | // ViewportKeyMap returns a set of pager-like default keybindings.
130 | func ViewportKeyMap() viewport.KeyMap {
131 | return viewport.KeyMap{
132 | PageDown: pageUp,
133 | PageUp: pageDown,
134 | HalfPageUp: cursorLeft,
135 | HalfPageDown: cursorRight,
136 | Up: cursorUp,
137 | Down: cursorDown,
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/load.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "io"
9 |
10 | "github.com/TylerBrock/colorjson"
11 |
12 | "github.com/paisano-nix/paisano/data"
13 | )
14 |
15 | func LoadJson(r io.Reader) (*data.Root, error) {
16 | var root = &data.Root{}
17 |
18 | var r2 bytes.Buffer
19 | r1 := io.TeeReader(r, &r2)
20 |
21 | var dec = json.NewDecoder(r1)
22 |
23 | if err := dec.Decode(&root.Cells); err != nil {
24 | var serr *json.SyntaxError
25 | if errors.As(err, &serr) {
26 | return nil, fmt.Errorf("json syntax error: %w: string:\n%v", err, r2.String())
27 | }
28 | var obj interface{}
29 | var debugDecoder = json.NewDecoder(&r2)
30 | debugDecoder.Decode(&obj)
31 | f := colorjson.NewFormatter()
32 | f.Indent = 2
33 | s, _ := f.Marshal(obj)
34 | return nil, fmt.Errorf("%w - object: %s", err, s)
35 | }
36 |
37 | return root, nil
38 | }
39 |
--------------------------------------------------------------------------------
/src/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "os"
6 |
7 | tea "github.com/charmbracelet/bubbletea"
8 | )
9 |
10 | var (
11 | buildVersion = "dev"
12 | buildCommit = "dirty"
13 | argv0 = "paisano"
14 | project = "Paisano"
15 | )
16 |
17 | func main() {
18 | if len(os.Args[1:]) == 0 {
19 | // with NO arguments, invoke the TUI
20 | if model, err := tea.NewProgram(
21 | InitialPage(),
22 | tea.WithAltScreen(),
23 | ).StartReturningModel(); err != nil {
24 | log.Fatalf("Error running program: %s", err)
25 | } else if err := model.(*Tui).FatalError; err != nil {
26 | log.Fatal(err)
27 | } else if command := model.(*Tui).ExecveCommand; command != nil {
28 | if err := command.Exec(nil); err != nil {
29 | log.Fatal(err)
30 | }
31 | }
32 | } else {
33 | // with arguments, invoke the CLI
34 | ExecuteCli()
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/models/readme.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/charmbracelet/bubbles/help"
8 | "github.com/charmbracelet/bubbles/key"
9 | tea "github.com/charmbracelet/bubbletea"
10 | "github.com/charmbracelet/lipgloss"
11 | "github.com/knipferrc/teacup/markdown"
12 |
13 | "github.com/paisano-nix/paisano/data"
14 | "github.com/paisano-nix/paisano/keys"
15 | "github.com/paisano-nix/paisano/styles"
16 | )
17 |
18 | var (
19 |
20 | // Tabs.
21 |
22 | activeTabBorder = lipgloss.Border{
23 | Top: "─",
24 | Bottom: " ",
25 | Left: "│",
26 | Right: "│",
27 | TopLeft: "╭",
28 | TopRight: "╮",
29 | BottomLeft: "┘",
30 | BottomRight: "└",
31 | }
32 |
33 | tabBorder = lipgloss.Border{
34 | Top: "─",
35 | Bottom: "─",
36 | Left: "│",
37 | Right: "│",
38 | TopLeft: "╭",
39 | TopRight: "╮",
40 | BottomLeft: "┴",
41 | BottomRight: "┴",
42 | }
43 |
44 | tab = lipgloss.NewStyle().
45 | Border(tabBorder, true).
46 | BorderForeground(styles.Highlight).
47 | Padding(0, 1)
48 |
49 | activeTab = tab.Copy().Border(activeTabBorder, true)
50 |
51 | tabGap = tab.Copy().
52 | BorderTop(false).
53 | BorderLeft(false).
54 | BorderRight(false)
55 | )
56 |
57 | type ReadmeModel struct {
58 | TargetHelp markdown.Model
59 | CellHelp markdown.Model
60 | BlockHelp markdown.Model
61 | Cell string
62 | Block string
63 | Target string
64 | Width int
65 | Height int
66 | KeyMap *keys.ReadmeKeyMap
67 | Help help.Model
68 | // Focus
69 | }
70 |
71 | type renderCellMarkdownMsg struct {
72 | msg tea.Msg
73 | }
74 | type renderBlockMarkdownMsg struct {
75 | msg tea.Msg
76 | }
77 | type renderTargetMarkdownMsg struct {
78 | msg tea.Msg
79 | }
80 |
81 | func (m *ReadmeModel) LoadReadme(d *data.Root, ci, bi, ti int) {
82 | m.Cell = d.CellName(ci, bi, ti)
83 | m.CellHelp.Viewport.SetContent(d.CellHelp(ci, bi, ti))
84 | m.Block = d.BlockName(ci, bi, ti)
85 | m.BlockHelp.Viewport.SetContent(d.BlockHelp(ci, bi, ti))
86 | m.Target = d.TargetName(ci, bi, ti)
87 | m.TargetHelp.Viewport.SetContent(d.TargetHelp(ci, bi, ti))
88 | }
89 |
90 | func NewReadme() *ReadmeModel {
91 | var (
92 | th = markdown.New(false, true, lipgloss.AdaptiveColor{})
93 | ch = markdown.New(false, true, lipgloss.AdaptiveColor{})
94 | oh = markdown.New(false, true, lipgloss.AdaptiveColor{})
95 | )
96 | th.Viewport.KeyMap = keys.ViewportKeyMap()
97 | ch.Viewport.KeyMap = keys.ViewportKeyMap()
98 | oh.Viewport.KeyMap = keys.ViewportKeyMap()
99 | return &ReadmeModel{
100 | TargetHelp: th,
101 | CellHelp: ch,
102 | BlockHelp: oh,
103 | Help: help.New(),
104 | KeyMap: keys.NewReadmeKeyMap(),
105 | }
106 | }
107 | func (m *ReadmeModel) Init() tea.Cmd {
108 | return nil
109 | }
110 |
111 | func (m *ReadmeModel) RenderMarkdown(d *data.Root, ci, bi, ti int) tea.Cmd {
112 | var (
113 | cmds []tea.Cmd
114 | cmd tea.Cmd
115 | )
116 | m.LoadReadme(d, ci, bi, ti)
117 | m.TargetHelp.SetIsActive(true)
118 | if d.HasCellHelp(ci, bi, ti) {
119 | cmd = func() tea.Msg {
120 | return renderCellMarkdownMsg{
121 | m.CellHelp.SetFileName(
122 | d.CellHelp(ci, bi, ti),
123 | )(),
124 | }
125 | }
126 | cmds = append(cmds, cmd)
127 | }
128 | if d.HasBlockHelp(ci, bi, ti) {
129 | cmd = func() tea.Msg {
130 | return renderBlockMarkdownMsg{
131 | m.BlockHelp.SetFileName(
132 | d.BlockHelp(ci, bi, ti),
133 | )(),
134 | }
135 | }
136 | cmds = append(cmds, cmd)
137 | }
138 | if d.HasTargetHelp(ci, bi, ti) {
139 | cmd = func() tea.Msg {
140 | return renderTargetMarkdownMsg{
141 | m.TargetHelp.SetFileName(
142 | d.TargetHelp(ci, bi, ti),
143 | )(),
144 | }
145 | }
146 | cmds = append(cmds, cmd)
147 | }
148 | return tea.Batch(cmds...)
149 | }
150 |
151 | func (m *ReadmeModel) Update(msg tea.Msg) (*ReadmeModel, tea.Cmd) {
152 | var (
153 | cmd tea.Cmd
154 | )
155 | switch msg := msg.(type) {
156 | case tea.KeyMsg:
157 | switch {
158 | case key.Matches(msg, m.KeyMap.CycleTab):
159 | if m.TargetHelp.Active {
160 | m.TargetHelp.SetIsActive(false)
161 | m.CellHelp.SetIsActive(true)
162 | } else if m.CellHelp.Active {
163 | m.CellHelp.SetIsActive(false)
164 | m.BlockHelp.SetIsActive(true)
165 | } else if m.BlockHelp.Active {
166 | m.BlockHelp.SetIsActive(false)
167 | m.TargetHelp.SetIsActive(true)
168 | }
169 | return m, nil
170 | case key.Matches(msg, m.KeyMap.ReverseCycleTab):
171 | if m.TargetHelp.Active {
172 | m.TargetHelp.SetIsActive(false)
173 | m.BlockHelp.SetIsActive(true)
174 | } else if m.CellHelp.Active {
175 | m.CellHelp.SetIsActive(false)
176 | m.TargetHelp.SetIsActive(true)
177 | } else if m.BlockHelp.Active {
178 | m.BlockHelp.SetIsActive(false)
179 | m.CellHelp.SetIsActive(true)
180 | }
181 | return m, nil
182 | }
183 | case tea.WindowSizeMsg:
184 | m.CellHelp.SetSize(m.Width, m.Height)
185 | m.BlockHelp.SetSize(m.Width, m.Height)
186 | m.TargetHelp.SetSize(m.Width, m.Height)
187 | case renderCellMarkdownMsg:
188 | m.CellHelp, cmd = m.CellHelp.Update(msg.msg)
189 | return m, cmd
190 | case renderBlockMarkdownMsg:
191 | m.BlockHelp, cmd = m.BlockHelp.Update(msg.msg)
192 | return m, cmd
193 | case renderTargetMarkdownMsg:
194 | m.TargetHelp, cmd = m.TargetHelp.Update(msg.msg)
195 | return m, cmd
196 | }
197 | if m.TargetHelp.Active {
198 | m.TargetHelp, cmd = m.TargetHelp.Update(msg)
199 | } else if m.CellHelp.Active {
200 | m.CellHelp, cmd = m.CellHelp.Update(msg)
201 | } else if m.BlockHelp.Active {
202 | m.BlockHelp, cmd = m.BlockHelp.Update(msg)
203 | }
204 | return m, cmd
205 | }
206 |
207 | func (m *ReadmeModel) View() string {
208 | // Tabs
209 | var (
210 | tabs []string
211 | content string
212 | )
213 | if m.CellHelp.Active {
214 | tabs = append(tabs, activeTab.Render(fmt.Sprintf("Cell: %s", m.Cell)))
215 | content = m.CellHelp.View()
216 | } else {
217 | tabs = append(tabs, tab.Render(fmt.Sprintf("Cell: %s", m.Cell)))
218 | }
219 | if m.BlockHelp.Active {
220 | tabs = append(tabs, activeTab.Render(fmt.Sprintf("Block: %s", m.Block)))
221 | content = m.BlockHelp.View()
222 | } else {
223 | tabs = append(tabs, tab.Render(fmt.Sprintf("Block: %s", m.Block)))
224 | }
225 | if m.TargetHelp.Active {
226 | tabs = append(tabs, activeTab.Render(fmt.Sprintf("Target: %s", m.Target)))
227 | content = m.TargetHelp.View()
228 | } else {
229 | tabs = append(tabs, tab.Render(fmt.Sprintf("Target: %s", m.Target)))
230 | }
231 |
232 | row := lipgloss.JoinHorizontal(lipgloss.Top, tabs...)
233 | gap := tabGap.Render(strings.Repeat(" ", max(0, m.Width-lipgloss.Width(row)-2)))
234 | row = lipgloss.JoinHorizontal(lipgloss.Bottom, row, gap)
235 |
236 | return lipgloss.JoinVertical(lipgloss.Top, row, content)
237 | }
238 |
239 | func (m *ReadmeModel) ShortHelp() []key.Binding {
240 | kb := []key.Binding{
241 | m.KeyMap.Up,
242 | m.KeyMap.Down,
243 | m.KeyMap.HalfPageUp,
244 | m.KeyMap.HalfPageDown,
245 | m.KeyMap.CycleTab,
246 | m.KeyMap.CloseReadme,
247 | }
248 | return kb
249 | }
250 |
251 | func (m *ReadmeModel) FullHelp() [][]key.Binding {
252 | kb := [][]key.Binding{{}}
253 | return kb
254 | }
255 | func max(a, b int) int {
256 | if a > b {
257 | return a
258 | }
259 | return b
260 | }
261 |
--------------------------------------------------------------------------------
/src/styles/styles.go:
--------------------------------------------------------------------------------
1 | package styles
2 |
3 | import (
4 | "github.com/charmbracelet/lipgloss"
5 | )
6 |
7 | var (
8 | Highlight = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"}
9 | AppStyle = lipgloss.NewStyle().Padding(1, 2)
10 |
11 | ErrorStyle = lipgloss.NewStyle().
12 | BorderStyle(lipgloss.NormalBorder()).
13 | BorderForeground(Highlight).Padding(0, 1)
14 |
15 | TargetStyle = lipgloss.NewStyle().
16 | BorderStyle(lipgloss.NormalBorder()).
17 | BorderForeground(Highlight)
18 |
19 | ActionInspectionStyle = lipgloss.NewStyle().
20 | BorderStyle(lipgloss.NormalBorder()).
21 | BorderForeground(Highlight).Padding(0, 1).Faint(true)
22 |
23 | ActionStyle = lipgloss.NewStyle().
24 | BorderStyle(lipgloss.NormalBorder()).
25 | BorderForeground(Highlight)
26 |
27 | ReadmeStyle = lipgloss.NewStyle().
28 | // BorderStyle(lipgloss.NormalBorder()).
29 | BorderForeground(Highlight)
30 |
31 | LegendStyle = lipgloss.NewStyle().Padding(1, 0, 0, 2)
32 |
33 | TitleStyle = lipgloss.NewStyle().
34 | Foreground(Highlight).Bold(true).
35 | Padding(1, 1)
36 |
37 | CacheWarning = lipgloss.NewStyle().
38 | Foreground(Highlight).Bold(true).MarginLeft(4)
39 | )
40 |
--------------------------------------------------------------------------------
/src/tui.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "strings"
8 |
9 | "github.com/aymanbagabas/go-osc52"
10 | "github.com/charmbracelet/bubbles/help"
11 | "github.com/charmbracelet/bubbles/key"
12 | "github.com/charmbracelet/bubbles/list"
13 | "github.com/charmbracelet/bubbles/spinner"
14 | tea "github.com/charmbracelet/bubbletea"
15 | "github.com/charmbracelet/lipgloss"
16 |
17 | "github.com/paisano-nix/paisano/data"
18 | "github.com/paisano-nix/paisano/flake"
19 | "github.com/paisano-nix/paisano/keys"
20 | "github.com/paisano-nix/paisano/models"
21 | "github.com/paisano-nix/paisano/styles"
22 | )
23 |
24 | type Focus int64
25 | type Loaded int64
26 |
27 | const (
28 | Left Focus = iota
29 | Right
30 | Readme
31 | Inspect
32 |
33 | FromFlake Loaded = iota
34 | FromCache
35 | Loading
36 | )
37 |
38 | var (
39 | cmdTemplate = func(target, action string) string { return fmt.Sprintf("%[1]s %[2]s:%[3]s", argv0, target, action) }
40 | )
41 |
42 | func (s Focus) String() string {
43 | switch s {
44 | case Left:
45 | return "left focus"
46 | case Right:
47 | return "right focus"
48 | }
49 | return "unknown"
50 | }
51 |
52 | type Targets = list.Model
53 |
54 | type TargetItem struct {
55 | r *data.Root
56 | CellIdx int
57 | BlockIdx int
58 | TargetIdx int
59 | }
60 |
61 | func (i TargetItem) Title() string { return i.r.TargetTitle(i.CellIdx, i.BlockIdx, i.TargetIdx) }
62 | func (i TargetItem) Description() string {
63 | return i.r.TargetDescription(i.CellIdx, i.BlockIdx, i.TargetIdx)
64 | }
65 | func (i TargetItem) FilterValue() string { return i.Title() }
66 |
67 | type Actions = list.Model
68 |
69 | type ActionItem struct {
70 | r *data.Root
71 | CellIdx int
72 | BlockIdx int
73 | TargetIdx int
74 | ActionIdx int
75 | }
76 |
77 | func (i ActionItem) Title() string {
78 | return i.r.ActionTitle(i.CellIdx, i.BlockIdx, i.TargetIdx, i.ActionIdx)
79 | }
80 | func (i ActionItem) Description() string {
81 | return i.r.ActionDescription(i.CellIdx, i.BlockIdx, i.TargetIdx, i.ActionIdx)
82 | }
83 | func (i ActionItem) FilterValue() string { return i.Title() }
84 |
85 | type Tui struct {
86 | r *data.Root
87 |
88 | Left Targets
89 | Right Actions
90 | Readme *models.ReadmeModel
91 | Legend help.Model
92 | Keys *keys.AppKeyMap
93 | Title string
94 | InspectAction string
95 | ExecveCommand *flake.RunActionCmd
96 | Spinner spinner.Model
97 | FatalError error
98 | Loaded
99 | Focus
100 | Width int
101 | Height int
102 | }
103 |
104 | func (m *Tui) LoadTargets() tea.Cmd {
105 | var (
106 | numItems = m.r.Len()
107 | counter = 0
108 | )
109 | // Make list of actions
110 | items := make([]list.Item, numItems)
111 | for ci, c := range m.r.Cells {
112 | for bi, b := range c.Blocks {
113 | for ti := range b.Targets {
114 | items[counter] = &TargetItem{m.r, ci, bi, ti}
115 | counter += 1
116 | }
117 | }
118 | }
119 | cmd := m.Left.SetItems(items)
120 | if m.Left.SelectedItem() != nil {
121 | target := m.Left.SelectedItem().(*TargetItem)
122 | cmd = tea.Batch(cmd, m.LoadActions(target))
123 | }
124 | return cmd
125 | }
126 |
127 | func (m *Tui) LoadActions(i *TargetItem) tea.Cmd {
128 | _, _, t := m.r.Select(i.CellIdx, i.BlockIdx, i.TargetIdx)
129 | var numItems = len(t.Actions)
130 | // Make list of actions
131 | items := make([]list.Item, numItems)
132 | for j := 0; j < numItems; j++ {
133 | items[j] = &ActionItem{m.r, i.CellIdx, i.BlockIdx, i.TargetIdx, j}
134 | }
135 | return m.Right.SetItems(items)
136 | }
137 |
138 | func (m *Tui) SetTitle() {
139 |
140 | if m.Right.SelectedItem() != nil {
141 | m.Title = cmdTemplate(
142 | m.Left.SelectedItem().(*TargetItem).Title(),
143 | m.Right.SelectedItem().(*ActionItem).Title(),
144 | )
145 | } else {
146 | m.Title = lipgloss.NewStyle().Faint(true).Render(cmdTemplate(m.Left.SelectedItem().(*TargetItem).Title(), "n/a"))
147 | }
148 | }
149 |
150 | func (m *Tui) SetInspect() (tea.Model, tea.Cmd) {
151 | if i, ok := m.Right.SelectedItem().(*ActionItem); ok {
152 | cmd := flake.RunActionCmd{
153 | ShowCmdStr: false,
154 | CmdStr: "",
155 | System: "", // unlike in the CLI, we don't implement to specifiy system in the TUI
156 | Cell: i.r.CellName(i.CellIdx, i.BlockIdx, i.TargetIdx),
157 | Block: i.r.BlockName(i.CellIdx, i.BlockIdx, i.TargetIdx),
158 | Target: i.r.TargetName(i.CellIdx, i.BlockIdx, i.TargetIdx),
159 | Action: i.r.ActionTitle(i.CellIdx, i.BlockIdx, i.TargetIdx, i.ActionIdx),
160 | RequiresArgs: i.r.ActionRequiresArgs(i.CellIdx, i.BlockIdx, i.TargetIdx, i.ActionIdx),
161 | }
162 | _, args, _ := cmd.Assemble(nil)
163 | m.InspectAction = "nix build " + strings.Join(args, " \\\n")
164 | return m, nil
165 | } else {
166 | m.InspectAction = ""
167 | return m, nil
168 | }
169 | }
170 |
171 | type cellLoadedFromCacheMsg struct{ root *data.Root }
172 | type cellLoadedMsg struct{ root *data.Root }
173 | type cellLoadingFatalErrMsg struct{ err error }
174 |
175 | func (m *Tui) Init() tea.Cmd {
176 | var cmds []tea.Cmd
177 | c, key, cmd, buf, err := flake.LoadFlakeCmd()
178 | if err != nil {
179 | return func() tea.Msg { return cellLoadingFatalErrMsg{err} }
180 | }
181 | cached, _, err := c.GetBytes(*key)
182 | if err == nil {
183 | // a cache hit ...
184 | // ... load the cache
185 | cmds = append(cmds, func() tea.Msg {
186 | root, err := LoadJson(bytes.NewReader(cached))
187 | if err != nil {
188 | return cellLoadingFatalErrMsg{err}
189 | }
190 | return cellLoadedFromCacheMsg{root}
191 | })
192 | // ... re-load the flake with non-blocking i/o
193 | cmds = append(cmds, func() tea.Msg {
194 | stderr := new(bytes.Buffer)
195 | cmd.Stderr = stderr
196 | err = cmd.Run()
197 | if err != nil {
198 | return cellLoadingFatalErrMsg{fmt.Errorf(stderr.String())}
199 | }
200 | bufA := &bytes.Buffer{}
201 | r := io.TeeReader(buf, bufA)
202 | root, err := LoadJson(r)
203 | // renew cache under all circumstances (might have updated)
204 | c.PutBytes(*key, bufA.Bytes())
205 | if err != nil {
206 | return cellLoadingFatalErrMsg{err}
207 | }
208 | return cellLoadedMsg{root}
209 | })
210 | } else {
211 | // on cache miss ...
212 | // ... load the flake with blocking i/o
213 | cmds = append(cmds, tea.ExecProcess(cmd, func(err error) tea.Msg {
214 | if err != nil {
215 | return cellLoadingFatalErrMsg{err}
216 | }
217 | bufA := &bytes.Buffer{}
218 | r := io.TeeReader(buf, bufA)
219 | root, err := LoadJson(r)
220 | c.PutBytes(*key, bufA.Bytes())
221 | if err != nil {
222 | return cellLoadingFatalErrMsg{err}
223 | }
224 | return cellLoadedMsg{root}
225 | }))
226 | }
227 | cmds = append(cmds, m.Spinner.Tick)
228 | return tea.Batch(cmds...)
229 | }
230 |
231 | func (m *Tui) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
232 | var (
233 | cmds []tea.Cmd
234 | cmd tea.Cmd
235 | actionKeys = keys.NewActionDelegateKeyMap()
236 | )
237 | switch msg := msg.(type) {
238 | case cellLoadedMsg:
239 | m.r = msg.root
240 | m.Loaded = FromFlake
241 | return m, tea.Batch(
242 | m.LoadTargets(),
243 | m.Left.StartSpinner(),
244 | )
245 |
246 | case cellLoadedFromCacheMsg:
247 | m.r = msg.root
248 | m.Loaded = FromCache
249 | return m, tea.Batch(
250 | m.LoadTargets(),
251 | m.Left.StartSpinner(),
252 | )
253 |
254 | case cellLoadingFatalErrMsg:
255 | m.FatalError = msg.err
256 | return m, tea.Quit
257 |
258 | case spinner.TickMsg:
259 | if m.Loaded != Loading {
260 | m.Spinner, cmd = m.Spinner.Update(msg)
261 | return m, cmd
262 | }
263 | m.Left, cmd = m.Left.Update(msg)
264 | cmds = append(cmds, cmd)
265 | m.Right, cmd = m.Right.Update(msg)
266 | cmds = append(cmds, cmd)
267 | return m, tea.Batch(cmds...)
268 | case tea.KeyMsg:
269 | // quit even during filtering
270 | if key.Matches(msg, m.Keys.ForceQuit) {
271 | return m, tea.Quit
272 | }
273 | // Quit action inspection if enabled.
274 | if m.Focus == Inspect && key.Matches(msg, actionKeys.QuitInspect) {
275 | m.Focus = Right
276 | return m, nil
277 | }
278 | // Don't match any of the keys below if we're actively filtering.
279 | if m.Left.FilterState() == list.Filtering {
280 | break
281 | }
282 | if key.Matches(msg, m.Keys.Quit) {
283 | return m, tea.Quit
284 | }
285 | // Don't match any of the keys below if no target is selected.
286 | if m.Left.SelectedItem() == nil {
287 | return m, nil
288 | }
289 | switch {
290 | case m.Focus == Right && key.Matches(msg, actionKeys.Exec):
291 | if i, ok := m.Right.SelectedItem().(*ActionItem); ok {
292 | m.ExecveCommand = &flake.RunActionCmd{
293 | ShowCmdStr: true,
294 | CmdStr: cmdTemplate(
295 | m.Left.SelectedItem().(*TargetItem).Title(),
296 | i.Title(),
297 | ),
298 | System: "", // unlike in the CLI, we don't implement to specify system in the TUI
299 | Cell: i.r.CellName(i.CellIdx, i.BlockIdx, i.TargetIdx),
300 | Block: i.r.BlockName(i.CellIdx, i.BlockIdx, i.TargetIdx),
301 | Target: i.r.TargetName(i.CellIdx, i.BlockIdx, i.TargetIdx),
302 | Action: i.r.ActionTitle(i.CellIdx, i.BlockIdx, i.TargetIdx, i.ActionIdx),
303 | RequiresArgs: i.r.ActionRequiresArgs(i.CellIdx, i.BlockIdx, i.TargetIdx, i.ActionIdx),
304 | }
305 | return m, tea.Quit
306 | }
307 | case m.Focus == Right && key.Matches(msg, actionKeys.Copy):
308 | if _, ok := m.Right.SelectedItem().(*ActionItem); ok {
309 | osc52.Copy(cmdTemplate(
310 | m.Left.SelectedItem().(*TargetItem).Title(),
311 | m.Right.SelectedItem().(*ActionItem).Title(),
312 | ))
313 | return m, nil
314 | }
315 | case m.Focus == Inspect && key.Matches(msg, actionKeys.Copy):
316 | osc52.Copy(m.InspectAction)
317 | return m, nil
318 | // toggle the help
319 | case key.Matches(msg, m.Keys.ShowReadme):
320 | if m.Focus == Left {
321 | m.Focus = Readme
322 | var t = m.Left.SelectedItem().(*TargetItem)
323 | cmd = m.Readme.RenderMarkdown(m.r, t.CellIdx, t.BlockIdx, t.TargetIdx)
324 | return m, cmd
325 | }
326 | if m.Focus == Right {
327 | m.Focus = Inspect
328 | return m.SetInspect()
329 | }
330 | fallthrough
331 | case key.Matches(msg, m.Readme.KeyMap.CloseReadme):
332 | if m.Focus == Readme {
333 | m.Focus = Left
334 | m.Readme.CellHelp.SetIsActive(false)
335 | m.Readme.BlockHelp.SetIsActive(false)
336 | m.Readme.TargetHelp.SetIsActive(false)
337 | return m, nil
338 | }
339 |
340 | // toggle the focus
341 | case key.Matches(msg, m.Keys.ToggleFocus, m.Keys.FocusLeft, m.Keys.FocusRight):
342 | // Don't toggle the focus if we're showing the help.
343 | if m.Focus == Readme || m.Focus == Inspect {
344 | break
345 | }
346 | if m.Focus == Left {
347 | if key.Matches(msg, m.Keys.FocusLeft) {
348 | return m, nil
349 | }
350 | m.Focus = Right
351 | m.SetTitle()
352 | } else {
353 | if key.Matches(msg, m.Keys.FocusRight) {
354 | return m, nil
355 | }
356 | m.Focus = Left
357 | m.Title = ""
358 | }
359 | cmd = m.Left.ToggleSpinner()
360 | cmds = append(cmds, cmd)
361 | cmd = m.Right.ToggleSpinner()
362 | cmds = append(cmds, cmd)
363 | return m, tea.Batch(cmds...)
364 | }
365 | case tea.WindowSizeMsg:
366 | m.Width = msg.Width
367 | m.Height = msg.Height
368 | // size Target
369 | m.Left.SetHeight(msg.Height - 10)
370 | m.Left.SetWidth(msg.Width*2/3 - 10)
371 | // size Action
372 | m.Right.SetHeight(msg.Height - 10)
373 | m.Right.SetWidth(msg.Width*1/3 - 10)
374 | // size Readme
375 | m.Readme.Height = msg.Height - 10
376 | m.Readme.Width = msg.Width - 10
377 | m.Readme, _ = m.Readme.Update(msg)
378 | return m, nil
379 | }
380 | // route all other messages according to state
381 | if m.Focus == Readme {
382 | m.Readme, cmd = m.Readme.Update(msg)
383 | cmds = append(cmds, cmd)
384 | } else if m.Focus == Left {
385 | m.Left, cmd = m.Left.Update(msg)
386 | cmds = append(cmds, cmd)
387 | if m.Left.SelectedItem() != nil {
388 | var target = m.Left.SelectedItem().(*TargetItem)
389 | cmds = append(cmds, m.LoadActions(target))
390 | } else {
391 | cmds = append(cmds, m.Right.SetItems([]list.Item{}))
392 | }
393 | } else {
394 | m.Right, cmd = m.Right.Update(msg)
395 | m.SetTitle()
396 | cmds = append(cmds, cmd)
397 | _, cmd = m.SetInspect()
398 | cmds = append(cmds, cmd)
399 | }
400 | return m, tea.Batch(cmds...)
401 | }
402 |
403 | func (m *Tui) View() string {
404 | var title string
405 | var cacheWarning string
406 | if m.Loaded == Loading {
407 | title = styles.TitleStyle.Render("Loading " + m.Spinner.View())
408 | } else if m.Loaded == FromCache {
409 | title = styles.TitleStyle.Render(m.Title)
410 | cacheWarning = styles.CacheWarning.Render("Using cache, refreshing: " + m.Spinner.View())
411 | } else if m.Loaded == FromFlake {
412 | title = styles.TitleStyle.Render(m.Title)
413 | }
414 |
415 | placementClosure := func(s string) string {
416 | return lipgloss.Place(
417 | m.Width,
418 | m.Height,
419 | lipgloss.Center,
420 | lipgloss.Center,
421 | styles.AppStyle.MaxWidth(m.Width).MaxHeight(m.Height).Render(s),
422 | )
423 | }
424 |
425 | if m.Loaded == Loading {
426 | return placementClosure(title)
427 | }
428 | if m.Focus == Readme {
429 | return placementClosure(
430 | lipgloss.JoinVertical(
431 | lipgloss.Center,
432 | title,
433 | styles.ReadmeStyle.Render(m.Readme.View()),
434 | styles.LegendStyle.Render(m.Legend.View(m)),
435 | ),
436 | )
437 | }
438 |
439 | if m.Focus == Inspect {
440 | return placementClosure(
441 | lipgloss.JoinVertical(
442 | lipgloss.Center,
443 | title,
444 | lipgloss.JoinHorizontal(
445 | lipgloss.Left,
446 | styles.ActionInspectionStyle.Width(m.Left.Width()).Height(m.Left.Height()).Render(m.InspectAction),
447 | styles.ActionStyle.Render(m.Right.View()),
448 | ),
449 | lipgloss.JoinHorizontal(
450 | lipgloss.Center,
451 | styles.LegendStyle.Render(m.Legend.View(m)),
452 | cacheWarning,
453 | ),
454 | ),
455 | )
456 | }
457 |
458 | return placementClosure(
459 | lipgloss.JoinVertical(
460 | lipgloss.Center,
461 | title,
462 | lipgloss.JoinHorizontal(
463 | lipgloss.Left,
464 | styles.TargetStyle.Width(m.Left.Width()).Height(m.Left.Height()).Render(m.Left.View()),
465 | styles.ActionStyle.Width(m.Right.Width()).Height(m.Right.Height()).Render(m.Right.View()),
466 | ),
467 | lipgloss.JoinHorizontal(
468 | lipgloss.Center,
469 | styles.LegendStyle.Render(m.Legend.View(m)),
470 | cacheWarning,
471 | ),
472 | ),
473 | )
474 | }
475 |
476 | func (m *Tui) ShortHelp() []key.Binding {
477 | if m.Focus == Readme {
478 | return append(m.Readme.ShortHelp(), []key.Binding{
479 | m.Keys.Quit,
480 | }...)
481 | }
482 | if m.Focus == Left {
483 | // switch off the list's help
484 | m.Left.KeyMap.ShowFullHelp.SetEnabled(false)
485 | m.Left.KeyMap.CloseFullHelp.SetEnabled(false)
486 | if m.Left.FilterState() == list.Filtering {
487 | return m.Left.ShortHelp()
488 | } else {
489 | return append(m.Left.ShortHelp(), []key.Binding{
490 | m.Keys.ToggleFocus,
491 | m.Keys.ShowReadme,
492 | m.Keys.Quit,
493 | }...)
494 | }
495 | } else {
496 | // switch off the list's help
497 | m.Right.KeyMap.ShowFullHelp.SetEnabled(false)
498 | m.Right.KeyMap.CloseFullHelp.SetEnabled(false)
499 | return append(m.Right.ShortHelp(), []key.Binding{
500 | m.Keys.ToggleFocus,
501 | m.Keys.ShowReadme,
502 | m.Keys.Quit,
503 | }...)
504 | }
505 | }
506 |
507 | func (m *Tui) FullHelp() [][]key.Binding {
508 | kb := [][]key.Binding{{}}
509 | return kb
510 | }
511 |
512 | func InitialPage() *Tui {
513 |
514 | spin := spinner.New()
515 | spin.Spinner = spinner.Points
516 |
517 | return &Tui{
518 | Left: InitialTargets(),
519 | Right: NewActions(),
520 | Keys: keys.NewAppKeyMap(),
521 | Focus: Left,
522 | Readme: models.NewReadme(),
523 | Legend: help.New(),
524 | Loaded: Loading,
525 | Spinner: spin,
526 | }
527 | }
528 |
529 | func InitialTargets() Targets {
530 |
531 | targetList := list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0)
532 | targetList.Title = "Target"
533 | targetList.KeyMap = keys.DefaultListKeyMap()
534 | targetList.SetFilteringEnabled(true)
535 | targetList.StartSpinner()
536 | targetList.DisableQuitKeybindings()
537 | targetList.SetShowHelp(false)
538 |
539 | return targetList
540 | }
541 |
542 | func NewActions() Actions {
543 |
544 | newActionDelegate := func(keys *keys.ActionDelegateKeyMap) list.DefaultDelegate {
545 | d := list.NewDefaultDelegate()
546 |
547 | d.UpdateFunc = func(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
548 |
549 | help := []key.Binding{keys.Exec, keys.Copy}
550 | d.ShortHelpFunc = func() []key.Binding { return help }
551 | d.FullHelpFunc = func() [][]key.Binding { return [][]key.Binding{} }
552 |
553 | return d
554 | }
555 |
556 | actionDelegateKeys := keys.NewActionDelegateKeyMap()
557 | delegate := newActionDelegate(actionDelegateKeys)
558 | actionList := list.New([]list.Item{}, delegate, 0, 0)
559 | actionList.Title = "Actions"
560 | actionList.KeyMap = keys.DefaultListKeyMap()
561 | actionList.SetShowPagination(false)
562 | actionList.SetShowHelp(false)
563 | actionList.SetShowStatusBar(false)
564 | actionList.SetFilteringEnabled(false)
565 | actionList.DisableQuitKeybindings()
566 |
567 | return actionList
568 | }
569 |
--------------------------------------------------------------------------------
|