From a6c9dba25ad9204ebde302728eb2a75532497f37 Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 3 Feb 2023 12:14:41 +0000 Subject: initial commit --- .envrc | 1 + .gitignore | 1 + confirm.go | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ editing.go | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ flake.lock | 42 +++++++++++++++++++++++++++ flake.nix | 42 +++++++++++++++++++++++++++ go.mod | 9 ++++++ go.sum | 53 ++++++++++++++++++++++++++++++++++ main.go | 50 +++++++++++++++++++++++++++++++++ selecting.go | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++ styles.go | 12 ++++++++ 11 files changed, 463 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 confirm.go create mode 100644 editing.go create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 selecting.go create mode 100644 styles.go diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff51edf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.direnv \ No newline at end of file diff --git a/confirm.go b/confirm.go new file mode 100644 index 0000000..a7d4748 --- /dev/null +++ b/confirm.go @@ -0,0 +1,92 @@ +package main + +import ( + "fmt" + + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type confirmModel struct { + ready bool + doRun bool + selected item + viewport viewport.Model + returnTo selectingModel +} + +func switchToConfirm(m selectingModel) (confirmModel, tea.Cmd) { + model := confirmModel{ + selected: m.list.SelectedItem().(item), + returnTo: m, + } + return model, model.Init() +} + +func (m confirmModel) View() string { + if !m.ready { + return "Initialising..." + } + + return appStyle.Render(fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView())) +} + +func (m confirmModel) Init() tea.Cmd { + return func() tea.Msg { + // FIXME: Hacky way to get a WindowSizeMsg event sent + frame_h, frame_v := appStyle.GetFrameSize() + return tea.WindowSizeMsg{ + Height: m.returnTo.list.Height() + frame_v, + Width: m.returnTo.list.Width() + frame_h, + } + } +} + +func (m confirmModel) headerView() string { + return confirmTitleStyle.Render(m.selected.title) +} +func (m confirmModel) footerView() string { + out := areYouSureTextStyle.Render("Are you sure?") + out += subtleTextStyle.Render(" RET to continue, q to go back, ctrl+c to quit") + return out +} + +func (m confirmModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + + case "enter": + m.doRun = true + return m, tea.Quit + case "ctrl-c": + return m, tea.Quit + case "q": + return m.returnTo, nil + } + case tea.WindowSizeMsg: + padding_h, padding_v := appStyle.GetFrameSize() + headerHeight := lipgloss.Height(m.headerView()) + footerHeight := lipgloss.Height(m.footerView()) + verticalMarginHeight := headerHeight + footerHeight + padding_v + + if !m.ready { + m.viewport = viewport.New(msg.Width-padding_h, msg.Height-verticalMarginHeight) + m.viewport.YPosition = headerHeight + padding_v + 1 + m.viewport.SetContent(m.selected.contents) + m.ready = true + } else { + m.viewport.Width = msg.Width - padding_h + m.viewport.Height = msg.Height - verticalMarginHeight - padding_v + } + } + + // Handle keyboard and mouse events in the viewport + var cmd tea.Cmd + m.viewport, cmd = m.viewport.Update(msg) + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) +} diff --git a/editing.go b/editing.go new file mode 100644 index 0000000..9aead7a --- /dev/null +++ b/editing.go @@ -0,0 +1,83 @@ +package main + +import ( + "os" + "os/exec" + + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" +) + +type snippetDetails struct { + name string + prevName string + contents string +} + +type VimFinishedMsg struct { + snippet snippetDetails + err error +} + +func newItem() tea.Cmd { + return editContents("#!/bin/sh", snippetDetails{}) +} + +func editContents(existingContents string, snippet snippetDetails) tea.Cmd { + f, _ := os.CreateTemp("", "doe.sh") + name := f.Name() + f.WriteString(existingContents) + f.Close() + + c := exec.Command("vim", f.Name()) + + return tea.ExecProcess(c, func(err error) tea.Msg { + contents, _ := os.ReadFile(name) + snippet.contents = string(contents) + + return VimFinishedMsg{snippet, err} + }) +} + +type detailsModel struct { + returnTo tea.Model + snippet snippetDetails + ti textinput.Model +} + +func (m detailsModel) Init() tea.Cmd { + return nil +} + +func (m detailsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.Type { + case tea.KeyEnter: + return m, tea.Quit + case tea.KeyCtrlC, tea.KeyEsc: + return m, tea.Quit + } + } + m.ti, cmd = m.ti.Update(msg) + return m, cmd +} + +func (m detailsModel) View() string { + out := "Enter snippet name\n" + out += m.ti.View() + return appStyle.Render(out) +} + +func switchToDetails(returnTo tea.Model, snippet snippetDetails) (tea.Model, tea.Cmd) { + ti := textinput.New() + ti.Placeholder = "Snippet name" + ti.Focus() + ti.CharLimit = 250 + ti.SetValue(snippet.name) + + m := detailsModel{returnTo, snippet, ti} + return m, m.Init() +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..5f75a9e --- /dev/null +++ b/flake.lock @@ -0,0 +1,42 @@ +{ + "nodes": { + "flake-utils": { + "locked": { + "lastModified": 1667395993, + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1675237434, + "narHash": "sha256-YoFR0vyEa1HXufLNIFgOGhIFMRnY6aZ0IepZF5cYemo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "285b3ff0660640575186a4086e1f8dc0df2874b5", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-22.11", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..3f12c82 --- /dev/null +++ b/flake.nix @@ -0,0 +1,42 @@ +{ + description = "A command saver"; + + inputs = { + nixpkgs.url = "nixpkgs/nixos-22.11"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + lastModifiedDate = self.lastModifiedDate or self.lastModified or "19700101"; + version = builtins.substring 0 8 lastModifiedDate; + doe = pkgs.buildGoModule { + pname = "doe"; + inherit version; + + src = ./.; + + vendorSha256 = "sha256-pQpattmS9VmO3ZIQUFn66az8GSmB4IvYhTTCFn6SUmo="; + }; + in + { + + # Provide some binary packages for selected system types. + packages.default = doe; + apps.default = { + drv = doe; + name = "doe"; + }; + + devShells.default = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ + go + gopls + ]; + }; + } + + ); +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a0fc4ec --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module git.tardisproject.uk/tcmal/doe + +go 1.16 + +require ( + github.com/charmbracelet/bubbles v0.15.0 + github.com/charmbracelet/bubbletea v0.23.1 + github.com/charmbracelet/lipgloss v0.6.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..81d752c --- /dev/null +++ b/go.sum @@ -0,0 +1,53 @@ +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg= +github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= +github.com/charmbracelet/bubbles v0.15.0 h1:c5vZ3woHV5W2b8YZI1q7v4ZNQaPetfHuoHzx+56Z6TI= +github.com/charmbracelet/bubbles v0.15.0/go.mod h1:Y7gSFbBzlMpUDR/XM9MhZI374Q+1p1kluf1uLl8iK74= +github.com/charmbracelet/bubbletea v0.23.1 h1:CYdteX1wCiCzKNUlwm25ZHBIc1GXlYFyUIte8WPvhck= +github.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY= +github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk= +github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= +github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0= +github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= +github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..9656146 --- /dev/null +++ b/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "syscall" + + tea "github.com/charmbracelet/bubbletea" +) + +type item struct { + title string + contents string +} + +func (i item) Title() string { return i.title } +func (i item) Description() string { return i.contents } +func (i item) FilterValue() string { return i.title } + +func main() { + if e := runProgram(); e != nil { + fmt.Println(e) + os.Exit(1) + } +} + +func runProgram() error { + + model, err := tea.NewProgram(StartSelecting(), tea.WithMouseCellMotion()).Run() + if err != nil { + return fmt.Errorf("Error running program: %s", err) + } + if confirm, ok := model.(confirmModel); ok && confirm.doRun { + sh, err := exec.LookPath("sh") + if err != nil { + return fmt.Errorf("could not find sh: %s", err) + } + + args := []string{"-v", "-c"} + args = append(args, "set -v; "+confirm.selected.contents) + + err = syscall.Exec(sh, args, os.Environ()) + if err != nil { + return fmt.Errorf("could not exec: %s", err) + } + } + + return nil +} diff --git a/selecting.go b/selecting.go new file mode 100644 index 0000000..ddd821d --- /dev/null +++ b/selecting.go @@ -0,0 +1,78 @@ +package main + +import ( + "fmt" + "strconv" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" +) + +type selectingModel struct { + list list.Model +} + +func StartSelecting() selectingModel { + var s string + for i := 0; i < 100; i++ { + s += "echo " + strconv.Itoa(i) + "\n" + } + items := []list.Item{ + item{title: "Test command", contents: s}, + item{title: "vim", contents: "vim"}, + } + + list := list.New(items, list.NewDefaultDelegate(), 0, 0) + list.Title = "Groceries" + + return selectingModel{ + list: list, + } +} + +func (m selectingModel) Init() tea.Cmd { + return tea.EnterAltScreen +} + +func (m selectingModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + h, v := appStyle.GetFrameSize() + m.list.SetSize(msg.Width-h, msg.Height-v) + + case tea.KeyMsg: + // Don't match any of the keys below if we're actively filtering. + if m.list.FilterState() == list.Filtering { + break + } + + switch msg.String() { + + case "enter", " ": + return switchToConfirm(m) + case "+", "n": + return m, newItem() + } + + case VimFinishedMsg: + if msg.err != nil { + fmt.Println(msg.err) + return m, tea.Quit + } + + return switchToDetails(m, msg.snippet) + } + + // This will also call our delegate's update function. + newListModel, cmd := m.list.Update(msg) + m.list = newListModel + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +func (m selectingModel) View() string { + return appStyle.Render(m.list.View()) +} diff --git a/styles.go b/styles.go new file mode 100644 index 0000000..5732b00 --- /dev/null +++ b/styles.go @@ -0,0 +1,12 @@ +package main + +import "github.com/charmbracelet/lipgloss" + +var ( + appStyle = lipgloss.NewStyle().Padding(1, 2) + + confirmTitleStyle = lipgloss.NewStyle().Bold(true) + + areYouSureTextStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF4136")).Bold(true).Align(lipgloss.Center) + subtleTextStyle = lipgloss.NewStyle() +) -- cgit v1.2.3