New Upstream Release - jqp

Ready changes

Summary

Merged new upstream version: 0.4.0 (was: 0.3.0).

Diff

diff --git a/README.md b/README.md
index 980a715..3246af2 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,12 @@ brew install noahgorstein/tap/jqp
 sudo port install jqp
 ```
 
+### Arch Linux
+Available through the Arch User Repository as [jqp-bin](https://aur.archlinux.org/packages/jqp-bin).
+```bash
+yay -S jqp-bin
+```
+
 ### Github releases
 
 Download the relevant asset for your operating system from the latest Github release. Unpack it, then move the binary to somewhere accessible in your `PATH`, e.g. `mv ./jqp /usr/local/bin`.
@@ -45,7 +51,7 @@ Flags:
   -v, --version         version for jqp
 ```
 
-`jqp` also supports input from STDIN. 
+`jqp` also supports input from STDIN. STDIN takes precedence over the command line flag.
 
 ```
 ➜ curl "https://api.github.com/repos/stedolan/jq/issues?per_page=2" | jqp 
@@ -97,8 +103,10 @@ If a configuration option is present in both the configuration file and the comm
 ### Available Configuration Options
 
 ```yaml
-theme: "nord" # controls the color scheme
-file: "/path/to/input/file.json" # stdin takes precedence over command line flag and this option
+theme:
+  name: "nord" # controls the color scheme
+  chromaStyleOverrides: # override parts of the chroma style
+    kc: "#009900 underline" # keys use the chroma short names
 ```
 
 ## Themes
@@ -106,11 +114,25 @@ file: "/path/to/input/file.json" # stdin takes precedence over command line flag
 Themes can be specified on the command line via the `-t/--theme <themeName>` flag. You can also set a theme in your [configuration file](#configuration). 
 
 ```yaml
-theme: "monokai"
+theme:
+  name: "monokai"
 ```
 
 <img width="1624" alt="Screen Shot 2022-10-02 at 5 31 40 PM" src="https://user-images.githubusercontent.com/23270779/193477383-db5ca769-12bf-4fd0-b826-b1fd4086eac3.png">
 
+### Chroma Style Overrides
+
+Overrides to the chroma styles used for a theme can be configured in your [configuration file](#configuration). 
+
+For the list of short keys, see [`chroma.StandardTypes`](https://github.com/alecthomas/chroma/blob/d38b87110b078027006bc34aa27a065fa22295a1/types.go#L210-L308). To see which token to use for a value, see the [JSON lexer](https://github.com/alecthomas/chroma/blob/master/lexers/embedded/json.xml) (look for `<token>` tags). To see the color and what's used in the style you're using, look for your style in the chroma [styles directory](https://github.com/alecthomas/chroma/tree/master/styles).
+
+```yaml
+theme:
+  name: "monokai" # name is required to know which theme to override
+  chromaStyleOverrides:
+    kc: "#009900 underline"
+```
+
 Themes are broken up into [light](#light-themes) and [dark](#dark-themes) themes. Light themes work best in terminals with a light background and dark themes work best in a terminal with a dark background. If no theme is specified or a non-existant theme is provided, the default theme is used, which was created to work with both terminals with a light and dark background. 
 
 ### Light Themes
diff --git a/cmd/root.go b/cmd/root.go
index 4666a75..d51ffd4 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -5,29 +5,48 @@ import (
 	"fmt"
 	"os"
 
+	"github.com/alecthomas/chroma/v2"
 	"github.com/charmbracelet/bubbletea"
 	"github.com/noahgorstein/jqp/tui/bubbles/jqplayground"
 	"github.com/noahgorstein/jqp/tui/theme"
 	"github.com/spf13/cobra"
-	"github.com/spf13/pflag"
 	"github.com/spf13/viper"
 )
 
 var rootCmd = &cobra.Command{
-	Version:      "0.3.0",
+	Version:      "0.4.0",
 	Use:          "jqp",
 	Short:        "jqp is a TUI to explore jq",
 	Long:         `jqp is a TUI to explore the jq command line utility`,
 	SilenceUsage: true,
 	RunE: func(cmd *cobra.Command, args []string) error {
+		configTheme := viper.GetString(configKeysName.themeName)
+		if !cmd.Flags().Changed(flagsName.theme) {
+			flags.theme = configTheme
+		}
+		themeOverrides := viper.GetStringMapString(configKeysName.themeOverrides)
+
+		jqtheme, defaultTheme := theme.GetTheme(flags.theme)
+		// If not using the default theme, 
+		// and if theme specified is the same as in the config,
+		// which happens if the theme flag was used,
+		// apply chroma style overrides.
+		if !defaultTheme && configTheme == flags.theme && len(themeOverrides) > 0 {
+			// Reverse chroma.StandardTypes to be keyed by short string
+			chromaTypes := make(map[string]chroma.TokenType)
+			for tokenType, short := range chroma.StandardTypes {
+				chromaTypes[short] = tokenType
+			}
 
-		cmd.Flags().VisitAll(func(f *pflag.Flag) {
-			// Apply the viper config value to the flag when the flag is not set and viper has a value
-			if !f.Changed && viper.IsSet(f.Name) {
-				val := viper.Get(f.Name)
-				cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val))
+			builder := jqtheme.ChromaStyle.Builder()
+			for k, v := range themeOverrides {
+				builder.Add(chromaTypes[k], v)
 			}
-		})
+			style, err := builder.Build()
+			if err == nil {
+				jqtheme.ChromaStyle = style
+			}
+		}
 
 		if isInputFromPipe() {
 			stdin := streamToBytes(os.Stdin)
@@ -37,7 +56,7 @@ var rootCmd = &cobra.Command{
 				return errors.New("JSON is not valid")
 			}
 
-			bubble := jqplayground.New(stdin, "STDIN", theme.GetTheme(flags.theme))
+			bubble := jqplayground.New(stdin, "STDIN", jqtheme)
 			p := tea.NewProgram(bubble, tea.WithAltScreen())
 			if err := p.Start(); err != nil {
 				return err
@@ -69,7 +88,7 @@ var rootCmd = &cobra.Command{
 				return err
 			}
 
-			bubble := jqplayground.New(data, fi.Name(), theme.GetTheme(flags.theme))
+			bubble := jqplayground.New(data, fi.Name(), jqtheme)
 			p := tea.NewProgram(bubble, tea.WithAltScreen())
 
 			if err := p.Start(); err != nil {
@@ -107,15 +126,11 @@ func initConfig() {
 }
 
 var flags struct {
-	filepath string
-	theme    string
+	filepath, theme string
 }
 
 var flagsName = struct {
-	file       string
-	fileShort  string
-	theme      string
-	themeShort string
+	file, fileShort, theme, themeShort string
 }{
 	file:       "file",
 	fileShort:  "f",
@@ -123,6 +138,13 @@ var flagsName = struct {
 	themeShort: "t",
 }
 
+var configKeysName = struct {
+	themeName, themeOverrides string
+}{
+	themeName:      "theme.name",
+	themeOverrides: "theme.chromaStyleOverrides",
+}
+
 var cfgFile string
 
 func Execute() {
diff --git a/debian/changelog b/debian/changelog
index b14f1c0..0f62767 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+jqp (0.4.0-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Sun, 28 May 2023 20:34:36 -0000
+
 jqp (0.3.0-1) unstable; urgency=medium
 
   * New upstream release
diff --git a/tui/bubbles/inputdata/inputdata.go b/tui/bubbles/inputdata/inputdata.go
index e43574a..5b85e22 100644
--- a/tui/bubbles/inputdata/inputdata.go
+++ b/tui/bubbles/inputdata/inputdata.go
@@ -7,7 +7,6 @@ import (
 	"strings"
 
 	"github.com/alecthomas/chroma/v2"
-	"github.com/alecthomas/chroma/v2/quick"
 	"github.com/charmbracelet/bubbles/viewport"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/lipgloss"
@@ -34,7 +33,7 @@ func New(inputJson []byte, filename string, theme theme.Theme) Bubble {
 		Styles:          styles,
 		viewport:        v,
 		inputJson:       inputJson,
-		highlightedJson: highlightInputJson(inputJson, *theme.ChromaStyle),
+		highlightedJson: highlightInputJson(inputJson, theme.ChromaStyle),
 		filename:        filename,
 	}
 	return b
@@ -45,7 +44,7 @@ func (b *Bubble) SetBorderColor(color lipgloss.TerminalColor) {
 	b.Styles.infoStyle.BorderForeground(color)
 }
 
-func highlightInputJson(inputJson []byte, chromaStyle chroma.Style) *bytes.Buffer {
+func highlightInputJson(inputJson []byte, chromaStyle *chroma.Style) *bytes.Buffer {
 	var f interface{}
 	// TODO: error handling
 	json.Unmarshal(inputJson, &f)
@@ -55,7 +54,7 @@ func highlightInputJson(inputJson []byte, chromaStyle chroma.Style) *bytes.Buffe
 	json.Indent(&prettyJSON, []byte(inputJson), "", "    ")
 
 	buf := new(bytes.Buffer)
-	quick.Highlight(buf, prettyJSON.String(), "json", utils.GetTerminalColorSupport(), chromaStyle.Name)
+	utils.HighlightJson(buf, prettyJSON.String(), chromaStyle)
 
 	return buf
 }
diff --git a/tui/bubbles/jqplayground/commands.go b/tui/bubbles/jqplayground/commands.go
index 99248b0..ccaac8e 100644
--- a/tui/bubbles/jqplayground/commands.go
+++ b/tui/bubbles/jqplayground/commands.go
@@ -55,7 +55,7 @@ func (b *Bubble) executeQuery(ctx context.Context) tea.Cmd {
 			results.WriteString(fmt.Sprintf("%s\n", string(r)))
 		}
 
-		highlightedOutput := highlightJson([]byte(results.String()), *b.theme.ChromaStyle)
+		highlightedOutput := highlightJson([]byte(results.String()), b.theme.ChromaStyle)
 		return queryResultMsg{
 			rawResults:         results.String(),
 			highlightedResults: highlightedOutput.String(),
diff --git a/tui/bubbles/jqplayground/update.go b/tui/bubbles/jqplayground/update.go
index e4c005f..3fffab4 100644
--- a/tui/bubbles/jqplayground/update.go
+++ b/tui/bubbles/jqplayground/update.go
@@ -90,6 +90,7 @@ func (b Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				cmd = b.writeOutputToFile()
 				cmds = append(cmds, cmd)
 			} else if b.state == state.Query {
+				b.queryinput.RotateHistory()
 				b.state = state.Running
 				var ctx context.Context
 				ctx, b.cancel = context.WithCancel(context.Background())
diff --git a/tui/bubbles/jqplayground/util.go b/tui/bubbles/jqplayground/util.go
index ac1e813..6f444d6 100644
--- a/tui/bubbles/jqplayground/util.go
+++ b/tui/bubbles/jqplayground/util.go
@@ -5,7 +5,6 @@ import (
 	"encoding/json"
 
 	"github.com/alecthomas/chroma/v2"
-	"github.com/alecthomas/chroma/v2/quick"
 	"github.com/noahgorstein/jqp/tui/utils"
 )
 
@@ -14,7 +13,7 @@ func isValidJson(input []byte) bool {
 	return json.Unmarshal(input, &js) == nil
 }
 
-func highlightJson(input []byte, chromaStyle chroma.Style) *bytes.Buffer {
+func highlightJson(input []byte, chromaStyle *chroma.Style) *bytes.Buffer {
 
 	if isValidJson(input) {
 		var f interface{}
@@ -22,10 +21,10 @@ func highlightJson(input []byte, chromaStyle chroma.Style) *bytes.Buffer {
 		var prettyJSON bytes.Buffer
 		json.Indent(&prettyJSON, []byte(input), "", "    ")
 		buf := new(bytes.Buffer)
-		quick.Highlight(buf, prettyJSON.String(), "json", utils.GetTerminalColorSupport(), chromaStyle.Name)
+		utils.HighlightJson(buf, prettyJSON.String(), chromaStyle)
 		return buf
 	}
 	buf := new(bytes.Buffer)
-	quick.Highlight(buf, string(input), "json", utils.GetTerminalColorSupport(), chromaStyle.Name)
+	utils.HighlightJson(buf, string(input), chromaStyle)
 	return buf
 }
diff --git a/tui/bubbles/queryinput/queryinput.go b/tui/bubbles/queryinput/queryinput.go
index bbe2201..ecd7ea7 100644
--- a/tui/bubbles/queryinput/queryinput.go
+++ b/tui/bubbles/queryinput/queryinput.go
@@ -1,6 +1,7 @@
 package queryinput
 
 import (
+	"container/list"
 	"github.com/charmbracelet/bubbles/textinput"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/lipgloss"
@@ -10,6 +11,10 @@ import (
 type Bubble struct {
 	Styles    Styles
 	textinput textinput.Model
+
+	history         *list.List
+	historyMaxLen   int
+	historySelected *list.Element
 }
 
 func New(theme theme.Theme) Bubble {
@@ -26,6 +31,9 @@ func New(theme theme.Theme) Bubble {
 	return Bubble{
 		Styles:    s,
 		textinput: ti,
+
+		history:       list.New(),
+		historyMaxLen: 512,
 	}
 }
 
@@ -37,6 +45,14 @@ func (b Bubble) GetInputValue() string {
 	return b.textinput.Value()
 }
 
+func (b *Bubble) RotateHistory() {
+	b.history.PushFront(b.textinput.Value())
+	b.historySelected = b.history.Front()
+	if b.history.Len() > b.historyMaxLen {
+		b.history.Remove(b.history.Back())
+	}
+}
+
 func (b Bubble) Init() tea.Cmd {
 	return textinput.Blink
 }
@@ -51,6 +67,32 @@ func (b Bubble) View() string {
 }
 
 func (b Bubble) Update(msg tea.Msg) (Bubble, tea.Cmd) {
+	if msg, ok := msg.(tea.KeyMsg); ok {
+		switch msg.Type {
+		case tea.KeyUp:
+			if b.history.Len() == 0 {
+				break
+			}
+			n := b.historySelected.Next()
+			if n != nil {
+				b.textinput.SetValue(n.Value.(string))
+				b.textinput.CursorEnd()
+				b.historySelected = n
+			}
+		case tea.KeyDown:
+			if b.history.Len() == 0 {
+				break
+			}
+			p := b.historySelected.Prev()
+			if p != nil {
+				b.textinput.SetValue(p.Value.(string))
+				b.textinput.CursorEnd()
+				b.historySelected = p
+			}
+		case tea.KeyEnter:
+			b.RotateHistory()
+		}
+	}
 
 	var (
 		cmd  tea.Cmd
diff --git a/tui/theme/theme.go b/tui/theme/theme.go
index ffa6c98..2183466 100644
--- a/tui/theme/theme.go
+++ b/tui/theme/theme.go
@@ -427,11 +427,12 @@ var themeMap = map[string]Theme{
 	},
 }
 
-func GetTheme(theme string) Theme {
+// returns a theme by name, and true if default theme was returned
+func GetTheme(theme string) (Theme, bool) {
 	lowercasedTheme := strings.ToLower(strings.TrimSpace(theme))
 	if value, ok := themeMap[lowercasedTheme]; ok {
-		return value
+		return value, false
 	} else {
-		return getDefaultTheme()
+		return getDefaultTheme(), true
 	}
 }
diff --git a/tui/utils/utils.go b/tui/utils/utils.go
index 07bc16e..7be2ae5 100644
--- a/tui/utils/utils.go
+++ b/tui/utils/utils.go
@@ -1,12 +1,41 @@
 package utils
 
 import (
+	"io"
+
+	"github.com/alecthomas/chroma/v2"
+	"github.com/alecthomas/chroma/v2/formatters"
+	"github.com/alecthomas/chroma/v2/lexers"
+	"github.com/alecthomas/chroma/v2/styles"
 	"github.com/charmbracelet/lipgloss"
 	"github.com/muesli/termenv"
 )
 
+func HighlightJson(w io.Writer, source string, style *chroma.Style) error {
+	l := lexers.Get("json")
+	if l == nil {
+		l = lexers.Fallback
+	}
+	l = chroma.Coalesce(l)
+
+	f := formatters.Get(getTerminalColorSupport())
+	if f == nil {
+		f = formatters.Fallback
+	}
+
+	if style == nil {
+		style = styles.Fallback
+	}
+
+	it, err := l.Tokenise(nil, source)
+	if err != nil {
+		return err
+	}
+	return f.Format(w, style, it)
+}
+
 // returns a string used for chroma syntax highlighting
-func GetTerminalColorSupport() string {
+func getTerminalColorSupport() string {
 	switch lipgloss.ColorProfile() {
 	case termenv.Ascii:
 		return "terminal"

More details

Full run details

Historical runs