Skip to content

Commit

Permalink
Allow the shell to be used inline, rather than full screen
Browse files Browse the repository at this point in the history
  • Loading branch information
DomBlack committed Aug 22, 2023
1 parent f7912c4 commit 6d4d4e8
Show file tree
Hide file tree
Showing 11 changed files with 197 additions and 43 deletions.
15 changes: 15 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package config

import (
"context"

"github.com/DomBlack/bubble-shell/pkg/config/keymap"
"github.com/DomBlack/bubble-shell/pkg/config/styles"
)
Expand All @@ -24,12 +26,25 @@ type Config struct {
//
// If empty no filtering will be done
PackagesToFilterFromStack []string

// InlineShell will cause the shell to be rendered inline
// rather than taking over the whole terminal
InlineShell bool

// RootContext is the context that will be used for the
// commands when they run
RootContext context.Context

// PromptFunc is a function that returns the prompt to be used
PromptFunc func() string
}

// Default returns a default configuration for the shell
func Default() *Config {
return &Config{
HistoryFile: ".bubble-shell-history",
RootContext: context.Background(),
PromptFunc: func() string { return "> " },
KeyMap: keymap.Default,
Styles: styles.Default,
MaxStackFrames: 8,
Expand Down
12 changes: 5 additions & 7 deletions mode_command_entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func (c *CommandEntryMode) Enter(m Model) (Model, tea.Cmd) {
}

m.lookBackPartial = ""
m.input.Prompt = m.cfg.PromptFunc()
m.input.CursorEnd()
m.input.Focus()

Expand Down Expand Up @@ -64,11 +65,12 @@ func (c *CommandEntryMode) Update(m Model, msg tea.Msg) (Model, tea.Cmd) {
return m, tea.Quit
}

historyItem := history.NewItem(line, history.RunningStatus)
historyItem := history.NewItem(m.input.Prompt, line, history.RunningStatus)
m.input.SetValue("")
m.input.CursorEnd()

return m, tea.Batch(
return m, tea.Sequence(
m.Enter(&CommandRunningMode{}),
m.history.AppendItem(historyItem),
m.ExecuteCommand(historyItem),
)
Expand All @@ -83,11 +85,7 @@ func (c *CommandEntryMode) Update(m Model, msg tea.Msg) (Model, tea.Cmd) {
return m, m.Enter(&AutoCompleteMode{})

case key.Matches(msg, m.cfg.KeyMap.Cancel):
if m.currentCmdCancel != nil {
m.currentCmdCancel()
m.currentCmdCancel = nil
return m, nil
} else if m.input.Value() != "" {
if m.input.Value() != "" {
m.input.SetValue("")
m.input.CursorEnd()
return m, nil
Expand Down
60 changes: 60 additions & 0 deletions mode_command_running.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package shell

import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
)

type CommandRunningMode struct{ KeepInputContent bool }

var _ Mode = (*CommandRunningMode)(nil)

func (c *CommandRunningMode) Enter(m Model) (Model, tea.Cmd) {
return m, nil
}

func (c *CommandRunningMode) Leave(m Model) (Model, tea.Cmd) {
return m, nil
}

func (c *CommandRunningMode) Update(m Model, msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, m.cfg.KeyMap.Cancel):
if m.currentCmdCancel != nil {
m.currentCmdCancel()
m.currentCmdCancel = nil
return m, nil
} else {
return m, tea.Quit
}
}
}

return m, nil
}

func (c *CommandRunningMode) AdditionalView(m Model) string {
return ""
}

func (c *CommandRunningMode) ShortHelp(m Model, keyMap KeyMap) []key.Binding {
cancel := keyMap.Cancel

if m.currentCmdCancel != nil {
cancel.SetHelp(cancel.Help().Key, "stop executing command")
} else {
cancel.SetHelp(cancel.Help().Key, "quit")
}

return []key.Binding{
cancel,
}
}

func (c *CommandRunningMode) FullHelp(m Model, keyMap KeyMap) [][]key.Binding {
return [][]key.Binding{
c.ShortHelp(m, keyMap),
}
}
4 changes: 2 additions & 2 deletions mode_history_lookback.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ func (h *HistoryLookbackMode) Update(m Model, msg tea.Msg) (Model, tea.Cmd) {
switch {
case key.Matches(msg, m.cfg.KeyMap.ExecuteCommand):
line := m.input.Value()
historyItem := history.NewItem(line, history.RunningStatus)
historyItem := history.NewItem(m.input.Prompt, line, history.RunningStatus)

return m, tea.Sequence(
m.Enter(&CommandEntryMode{}),
m.Enter(&CommandRunningMode{}),
m.history.AppendItem(historyItem),
m.ExecuteCommand(historyItem),
)
Expand Down
4 changes: 2 additions & 2 deletions mode_history_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ func (h *HistorySearchMode) Update(m Model, msg tea.Msg) (Model, tea.Cmd) {
switch {
case key.Matches(msg, m.cfg.KeyMap.ExecuteCommand):
line := m.input.Value()
historyItem := history.NewItem(line, history.RunningStatus)
historyItem := history.NewItem(m.input.Prompt, line, history.RunningStatus)
return m, tea.Sequence(
m.Enter(&CommandEntryMode{}),
m.Enter(&CommandRunningMode{}),
m.history.AppendItem(historyItem),
m.ExecuteCommand(historyItem),
)
Expand Down
49 changes: 28 additions & 21 deletions model.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,34 +181,39 @@ func (m Model) View() string {
input := m.input.View()
modeView := m.mode.AdditionalView(m)

// Fit the history to the screen based on the output of the autocomplete and if we're showing the search input
neededHistoryHeight := m.height - 1 // one for the prompt
if modeView != "" {
neededHistoryHeight -= lipgloss.Height(modeView)
if !m.cfg.InlineShell {
// Fit the history to the screen based on the output of the autocomplete and if we're showing the search input
neededHistoryHeight := m.height - 1 // one for the prompt
if modeView != "" {
neededHistoryHeight -= lipgloss.Height(modeView)
}

for lipgloss.Height(historyView) > neededHistoryHeight {
firstNewLine := strings.IndexRune(historyView, '\n')
historyView = historyView[firstNewLine+1:]
}
}

for lipgloss.Height(historyView) > neededHistoryHeight {
firstNewLine := strings.IndexRune(historyView, '\n')
historyView = historyView[firstNewLine+1:]
// If we're an inline shell and the previous command is still running, wait for it to finish
lastCmd := m.history.Lookback(1)
if m.cfg.InlineShell && !lastCmd.LoadedHistory && !lastCmd.Started.IsZero() && lastCmd.Finished.IsZero() {
input = ""
}

// Now decide how to render the view
// Now render all the parts vertically
parts := make([]string, 0, 3)
parts = append(parts, historyView)
if input != "" {
parts = append(parts, input)
}
if modeView != "" {
return lipgloss.JoinVertical(lipgloss.Top,
historyView,
input,
modeView,
)
} else {
return lipgloss.JoinVertical(lipgloss.Top,
historyView,
input,
)
parts = append(parts, modeView)
}
return lipgloss.JoinVertical(lipgloss.Top, parts...)
}

func (m Model) ExecuteCommand(cmd history.Item) tea.Cmd {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(m.cfg.RootContext)

return tea.Batch(
func() tea.Msg {
Expand All @@ -229,8 +234,10 @@ func (m Model) ExecuteCommand(cmd history.Item) tea.Cmd {
// Capture the stdout
cmd.Output = strings.TrimSpace(string(stdoutBuffer.Bytes()))

// Create an UpdateItem [tea.Cmd] and then execute it
return m.history.UpdateItem(cmd)()
return tea.Sequence(
m.history.UpdateItem(cmd), // Create an UpdateItem [tea.Cmd]
m.Enter(&CommandEntryMode{}), // Then switch back to command entry mode
)()
},
)
}
Expand Down
35 changes: 35 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package shell

import (
"context"

"github.com/DomBlack/bubble-shell/internal/config"
"github.com/DomBlack/bubble-shell/pkg/config/keymap"
"github.com/DomBlack/bubble-shell/pkg/config/styles"
Expand Down Expand Up @@ -80,3 +82,36 @@ func WithStackTraceFilters(packages ...string) Option {
o.PackagesToFilterFromStack = packages
}
}

// WithInlineShell sets the shell to be inline rather than trying to render full screen
//
// This means that recovered history will not be shown, however your terminals own render
// will be incharge of scrolling.
func WithInlineShell() Option {
return func(o *config.Config) {
o.InlineShell = true
}
}

// WithBaseContext sets the context that commands will be run with
// when they are executed by users.
//
// By default [context.Background] will be used
func WithBaseContext(ctx context.Context) Option {
return func(o *config.Config) {
o.RootContext = ctx
}
}

// WithPromptFunc sets the function for rendering the prompt
//
// By default a function will be provided that returns "> "
func WithPromptFunc(promptFunc func() string) Option {
if promptFunc == nil {
panic("promptFunc cannot be nil")
}

return func(o *config.Config) {
o.PromptFunc = promptFunc
}
}
2 changes: 1 addition & 1 deletion pkg/tui/autocomplete/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func (m Model) View() string {

columns := make([][]string, numColumns)
for i, option := range m.options {
colNum := i / perColumn
colNum := i % perColumn

if i == m.selectedOption {
columns[colNum] = append(columns[colNum], m.selectedOptionStyle.Render(option.Name))
Expand Down
14 changes: 10 additions & 4 deletions pkg/tui/history/history_io.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ func (m Model) ReadHistory() tea.Cmd {
return nil, err
}
if part {
return nil, errors.New("line too long")
if m.cfg.InlineShell {
continue
} else {
return nil, errors.New("line too long")
}
}
if !utf8.Valid(line) {
return nil, errors.New("line isn't valid utf8")
Expand All @@ -73,6 +77,7 @@ func (m Model) ReadHistory() tea.Cmd {
if err := json.Unmarshal(line, &item); err != nil {
return nil, errors.Wrap(err, "unable to unmarshal history item")
}
item.LoadedHistory = true

history = append(history, item)
if len(history) > Limit {
Expand All @@ -82,7 +87,8 @@ func (m Model) ReadHistory() tea.Cmd {

// Mark the history as restored if there is any history
if len(history) > 0 {
item := NewItem("restored history from previous session", SuccessStatus)
item := NewItem("", "restored history from previous session", SuccessStatus)
item.LoadedHistory = true
item.ItemType = HistoryRestored
history = append(history, item)
}
Expand All @@ -93,7 +99,7 @@ func (m Model) ReadHistory() tea.Cmd {
return func() tea.Msg {
history, err := readHistory()
if err != nil {
item := NewItem("error loading history file", ErrorStatus)
item := NewItem("", "error loading history file", ErrorStatus)
item.ItemType = InternalError
item.Error = err

Expand Down Expand Up @@ -151,7 +157,7 @@ func (m Model) SaveHistory(items []Item) tea.Cmd {
return func() tea.Msg {
err := saveHistory()
if err != nil {
item := NewItem("error saving history file", ErrorStatus)
item := NewItem("", "error saving history file", ErrorStatus)
item.ItemType = InternalError
item.Error = err

Expand Down
16 changes: 12 additions & 4 deletions pkg/tui/history/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const (
type Item struct {
// This group of fields are serialized and stored
ID xid.ID `json:"id"` // The unique ID of the command
Prompt string `json:"prompt"` // The prompt that was displayed when the command was executed
Line string `json:"line"` // The command executed
Started time.Time `json:"started"` // The time the command was executed
Finished time.Time `json:"finished"` // The time the command finished executing
Expand All @@ -44,14 +45,16 @@ type Item struct {

// This group of fields are not serialized and are only used
// for rendering the UI during the current shell session
ItemType ItemType `json:"-"` // If true then this item is an internal error item and not a user command
Error error `json:"-"` // The error returned from the command
ItemType ItemType `json:"-"` // If true then this item is an internal error item and not a user command
Error error `json:"-"` // The error returned from the command
LoadedHistory bool `json:"-"` // If true then this item is a history restored item and not a user command
}

// NewItem creates a new history item with the given line and status
func NewItem(line string, status Status) Item {
func NewItem(prompt, line string, status Status) Item {
return Item{
ID: xid.New(),
Prompt: prompt,
Line: line,
Started: time.Now(),
Status: status,
Expand Down Expand Up @@ -94,7 +97,12 @@ func (i Item) View(cfg *config.Config, width int) string {
lines[0] = cfg.Styles.HistoricLine.Render(i.Line)
switch i.ItemType {
case Command:
lines[0] = cfg.Styles.HistoricPrompt.Render("> ") + lines[0]
prompt := i.Prompt
if prompt == "" {
prompt = "> "
}

lines[0] = cfg.Styles.HistoricPrompt.Render(prompt) + lines[0]

case InternalError:
lines[0] = cfg.Styles.InternalError.Render("!! ") + lines[0]
Expand Down
Loading

0 comments on commit 6d4d4e8

Please sign in to comment.