From ae9215722982fcb1b5d544875caad2de411309a4 Mon Sep 17 00:00:00 2001 From: visrosa Date: Tue, 9 Jun 2026 20:42:19 -0300 Subject: [PATCH] feat: introduce new canonical config key and action names via forward aliases Add oviewer/compat.go, which registers new canonical names as forward aliases for existing internal names across two config surfaces: - viper aliases map new YAML/env-var key names to existing internal keys, so configs written with new names load correctly without any internal rename. - Action dispatch aliases add new action string names alongside the existing ones: alternate_rows_mode, column_rainbow_mode, write_on_exit, hide_other_section, row_header, row_header_column, write_exit_range, debug_log. A deprecation warning is emitted to stderr for each old-name key detected in the loaded configuration (file, env var, or CLI flag). The check runs before alias registration so viper.IsSet reflects direct usage only. No internal names change; all existing config files, env vars, and custom keybind configs continue to work unchanged. Reference configs (ov.yaml, ov-less.yaml, ov.yaml.template) updated to use the new canonical names throughout. --- main.go | 2 + ov-less.yaml | 24 +++++----- ov.yaml | 24 +++++----- ov.yaml.template | 24 +++++----- oviewer/compat.go | 104 ++++++++++++++++++++++++++++++++++++++++ oviewer/keybind.go | 4 +- oviewer/keybind_test.go | 9 +++- 7 files changed, 152 insertions(+), 39 deletions(-) create mode 100644 oviewer/compat.go diff --git a/main.go b/main.go index c79d89a6..321dd68c 100644 --- a/main.go +++ b/main.go @@ -706,6 +706,8 @@ func initConfig() { } } + oviewer.RegisterCompatAliases(viper.GetViper()) + if err := viper.Unmarshal(&config); err != nil { fmt.Fprintln(os.Stderr, err) return diff --git a/ov-less.yaml b/ov-less.yaml index d6a8b2a2..deebbcaf 100644 --- a/ov-less.yaml +++ b/ov-less.yaml @@ -1,15 +1,15 @@ # This is the ov config file. # Copy it to `$XDG_CONFIG_HOME/ov/config.yaml` or start it with `ov --config ov.yaml`. # -# QuitSmall: false # Quit if the file size is smaller than the terminal size. -# IsWriteOriginal: false # Write the original content when exiting. -# BeforeWriteOriginal: 0 # Write the number of lines before the current location when exiting. -# AfterWriteOriginal: 0 # Write the number of lines after the current location when exiting. +# QuitIfOneScreen: false # Quit if the file size is smaller than the terminal size. +# WriteOriginal: false # Write the original content when exiting. +# WriteContextBefore: 0 # Write the number of lines before the current location when exiting. +# WriteContextAfter: 0 # Write the number of lines after the current location when exiting. # # CaseSensitive: false # Case sensitive search. # SmartCaseSensitive: false # Case sensitive search if the search string contains uppercase characters. # RegexpSearch: false # Regular expression search. -# Incsearch: true # Incremental search. +# IncrementalSearch: true # Incremental search. # # MemoryLimit: -1 # The maximum number of lines that can be loaded into memory. # MemoryLimitFile: 100 # The maximum number of lines that can be loaded into memory when opening a file. @@ -45,11 +45,11 @@ General: Header: 0 AlternateRows: false ColumnMode: false - LineNumMode: false + LineNumberMode: false Wrap: "character" # Wrap mode. Options: "character", "word", "none". ColumnDelimiter: "," MarkStyleWidth: 1 -# HScrollWidth: 10% +# HScrollStep: 10% # VScrollLines: 2 Prompt: Normal: @@ -82,7 +82,7 @@ General: Background: "darkgoldenrod" SectionLine: Background: "slateblue" - VerticalHeader: + RowHeader: # Add appropriate styles here Ruler: Background: "#333333" @@ -92,7 +92,7 @@ General: # Add appropriate styles here SectionHeaderBorder: # Add appropriate styles here - VerticalHeaderBorder: + RowHeaderBorder: Background: "#c0c0c0" MultiColorHighlight: - Foreground: "red" @@ -380,19 +380,19 @@ Mode: Header: 2 AlternateRows: true ColumnMode: true - LineNumMode: false + LineNumberMode: false Wrap: "character" ColumnDelimiter: "|" mysql: Header: 3 AlternateRows: true ColumnMode: true - LineNumMode: false + LineNumberMode: false Wrap: "character" ColumnDelimiter: "|" ps: Header: 1 ColumnMode: true ColumnRainbow: true - ColumnWidth: true + ColumnFixedWidth: true Wrap: "none" diff --git a/ov.yaml b/ov.yaml index 7a7f1a24..016816da 100644 --- a/ov.yaml +++ b/ov.yaml @@ -1,15 +1,15 @@ # This is the ov config file. # Copy it to `$XDG_CONFIG_HOME/ov/config.yaml` or start it with `ov --config ov.yaml`. # -# QuitSmall: false # Quit if the file size is smaller than the terminal size. -# IsWriteOriginal: false # Write the original content when exiting. -# BeforeWriteOriginal: 0 # Write the number of lines before the current location when exiting. -# AfterWriteOriginal: 0 # Write the number of lines after the current location when exiting. +# QuitIfOneScreen: false # Quit if the file size is smaller than the terminal size. +# WriteOriginal: false # Write the original content when exiting. +# WriteContextBefore: 0 # Write the number of lines before the current location when exiting. +# WriteContextAfter: 0 # Write the number of lines after the current location when exiting. # # CaseSensitive: false # Case sensitive search. # SmartCaseSensitive: false # Case sensitive search if the search string contains uppercase characters. # RegexpSearch: false # Regular expression search. -# Incsearch: true # Incremental search. +# IncrementalSearch: true # Incremental search. # # MemoryLimit: -1 # The maximum number of lines that can be loaded into memory. # MemoryLimitFile: 100 # The maximum number of lines that can be loaded into memory when opening a file. @@ -45,11 +45,11 @@ General: Header: 0 AlternateRows: false ColumnMode: false - LineNumMode: false + LineNumberMode: false Wrap: "character" # Wrap mode. Options: "character", "word", "none". ColumnDelimiter: "," MarkStyleWidth: 1 -# HScrollWidth: 10% +# HScrollStep: 10% # VScrollLines: 2 Prompt: Normal: @@ -82,7 +82,7 @@ General: Background: "darkgoldenrod" SectionLine: Background: "slateblue" - VerticalHeader: + RowHeader: # Add appropriate styles here Ruler: Background: "#333333" @@ -92,7 +92,7 @@ General: # Add appropriate styles here SectionHeaderBorder: # Add appropriate styles here - VerticalHeaderBorder: + RowHeaderBorder: Background: "#c0c0c0" MultiColorHighlight: - Foreground: "red" @@ -362,19 +362,19 @@ Mode: Header: 2 AlternateRows: true ColumnMode: true - LineNumMode: false + LineNumberMode: false Wrap: "character" ColumnDelimiter: "|" mysql: Header: 3 AlternateRows: true ColumnMode: true - LineNumMode: false + LineNumberMode: false Wrap: "character" ColumnDelimiter: "|" ps: Header: 1 ColumnMode: true ColumnRainbow: true - ColumnWidth: true + ColumnFixedWidth: true Wrap: "none" diff --git a/ov.yaml.template b/ov.yaml.template index 689b39aa..5b5d594a 100644 --- a/ov.yaml.template +++ b/ov.yaml.template @@ -1,15 +1,15 @@ # This is the ov config file. # Copy it to `$XDG_CONFIG_HOME/ov/config.yaml` or start it with `ov --config ov.yaml`. # -# QuitSmall: false # Quit if the file size is smaller than the terminal size. -# IsWriteOriginal: false # Write the original content when exiting. -# BeforeWriteOriginal: 0 # Write the number of lines before the current location when exiting. -# AfterWriteOriginal: 0 # Write the number of lines after the current location when exiting. +# QuitIfOneScreen: false # Quit if the file size is smaller than the terminal size. +# WriteOriginal: false # Write the original content when exiting. +# WriteContextBefore: 0 # Write the number of lines before the current location when exiting. +# WriteContextAfter: 0 # Write the number of lines after the current location when exiting. # # CaseSensitive: false # Case sensitive search. # SmartCaseSensitive: false # Case sensitive search if the search string contains uppercase characters. # RegexpSearch: false # Regular expression search. -# Incsearch: true # Incremental search. +# IncrementalSearch: true # Incremental search. # # MemoryLimit: -1 # The maximum number of lines that can be loaded into memory. # MemoryLimitFile: 100 # The maximum number of lines that can be loaded into memory when opening a file. @@ -45,11 +45,11 @@ General: Header: 0 AlternateRows: false ColumnMode: false - LineNumMode: false + LineNumberMode: false Wrap: "character" # Wrap mode. Options: "character", "word", "none". ColumnDelimiter: "," MarkStyleWidth: 1 -# HScrollWidth: 10% +# HScrollStep: 10% # VScrollLines: 2 Prompt: Normal: @@ -82,7 +82,7 @@ General: Background: "darkgoldenrod" SectionLine: Background: "slateblue" - VerticalHeader: + RowHeader: # Add appropriate styles here Ruler: Background: "#333333" @@ -92,7 +92,7 @@ General: # Add appropriate styles here SectionHeaderBorder: # Add appropriate styles here - VerticalHeaderBorder: + RowHeaderBorder: Background: "#c0c0c0" MultiColorHighlight: - Foreground: "red" @@ -143,19 +143,19 @@ Mode: Header: 2 AlternateRows: true ColumnMode: true - LineNumMode: false + LineNumberMode: false Wrap: "character" ColumnDelimiter: "|" mysql: Header: 3 AlternateRows: true ColumnMode: true - LineNumMode: false + LineNumberMode: false Wrap: "character" ColumnDelimiter: "|" ps: Header: 1 ColumnMode: true ColumnRainbow: true - ColumnWidth: true + ColumnFixedWidth: true Wrap: "none" diff --git a/oviewer/compat.go b/oviewer/compat.go new file mode 100644 index 00000000..06780fec --- /dev/null +++ b/oviewer/compat.go @@ -0,0 +1,104 @@ +package oviewer + +import ( + "context" + "fmt" + "os" + + "github.com/spf13/viper" +) + +// compatEntry holds the mapping between an existing internal config key name +// and its new canonical name, plus display names for deprecation messages. +type compatEntry struct { + oldKey string // existing viper key (lowercase, no change) + newKey string // new canonical viper key (lowercase) + oldName string // human-readable old key for deprecation message + newName string // human-readable new key for deprecation message +} + +// compatViperEntries enumerates every config key rename from the naming audit. +// oldKey is the existing internal name that stays unchanged; newKey is the new +// name being introduced. Deprecation warnings fire on oldKey; aliases are +// registered new→old so either name works in config files and env vars. +var compatViperEntries = []compatEntry{ + // A2: QuitSmall → QuitIfOneScreen (matches CLI --quit-if-one-screen) + {"quitsmall", "quitifonescreen", "QuitSmall", "QuitIfOneScreen"}, + // A3: LineNumMode → LineNumberMode (abbreviation → full word) + {"general.linenumode", "general.linenumbermode", "general.LineNumMode", "general.LineNumberMode"}, + // A5/B6: IsWriteOnExit → WriteOnExit (drop Go Is-prefix from YAML key) + {"iswriteonexit", "writeonexit", "IsWriteOnExit", "WriteOnExit"}, + // B6: IsWriteOriginal → WriteOriginal (drop Go Is-prefix from YAML key) + {"iswriteoriginal", "writeoriginal", "IsWriteOriginal", "WriteOriginal"}, + // A6: BeforeWriteOriginal → WriteContextBefore (drop impl-detail "Original") + {"beforewriteoriginal", "writecontextbefore", "BeforeWriteOriginal", "WriteContextBefore"}, + // A6: AfterWriteOriginal → WriteContextAfter (drop impl-detail "Original") + {"afterwriteoriginal", "writecontextafter", "AfterWriteOriginal", "WriteContextAfter"}, + // B1: VerticalHeader → RowHeader (misnomer: freezes row-identifying content, not column labels) + {"general.verticalheader", "general.rowheader", "general.VerticalHeader", "general.RowHeader"}, + // B1: HeaderColumn → RowHeaderColumn (paired rename with RowHeader) + {"general.headercolumn", "general.rowheadercolumn", "general.HeaderColumn", "general.RowHeaderColumn"}, + // B1: Style.VerticalHeader → Style.RowHeader + {"general.style.verticalheader", "general.style.rowheader", "general.style.VerticalHeader", "general.style.RowHeader"}, + // B1: Style.VerticalHeaderBorder → Style.RowHeaderBorder + {"general.style.verticalheaderborder", "general.style.rowheaderborder", "general.style.VerticalHeaderBorder", "general.style.RowHeaderBorder"}, + // B4: ColumnWidth → ColumnFixedWidth (clarify it switches detection method, not column width value) + {"general.columnwidth", "general.columnfixedwidth", "general.ColumnWidth", "general.ColumnFixedWidth"}, + // B5: Incsearch → IncrementalSearch (sole abbreviation among fully-spelled keys) + {"incsearch", "incrementalsearch", "Incsearch", "IncrementalSearch"}, + // B8: HScrollWidth → HScrollStep (it is a step size, not a display width) + {"general.hscrollwidth", "general.hscrollstep", "general.HScrollWidth", "general.HScrollStep"}, +} + +// compatActionEntries maps new canonical action names to existing internal ones. +// Users may bind either name in the keybind section of their config. +var compatActionEntries = map[string]string{ + // A1: alter_rows_mode → alternate_rows_mode (truncated word → full word) + "alternate_rows_mode": actionAlternate, + // A4: rainbow_mode → column_rainbow_mode (disambiguate from multi-color search highlight) + "column_rainbow_mode": actionRainbow, + // A5: write_exit → write_on_exit (match CLI --exit-write and YAML WriteOnExit) + "write_on_exit": actionWriteExit, + // A8: hide_other → hide_other_section (drop ambiguous truncation) + "hide_other_section": actionHideOther, + // B1: vertical_header → row_header (paired with config key rename) + "row_header": actionVerticalHeader, + // B1: header_column → row_header_column (paired with config key rename) + "row_header_column": actionHeaderColumn, + // D3: set_write_exit → write_exit_range (describes what it does: configure line range) + "write_exit_range": actionWriteBA, + // D4: logdoc → debug_log (opaque internal name → intent-revealing name) + "debug_log": actionLogDoc, +} + +// RegisterCompatAliases registers new canonical config key names as viper aliases +// for existing internal names and emits deprecation notices for any old names +// found in the loaded configuration. Must be called after viper.ReadInConfig +// and before viper.Unmarshal. +func RegisterCompatAliases(v *viper.Viper) { + warnDeprecatedKeys(v) + for _, e := range compatViperEntries { + v.RegisterAlias(e.newKey, e.oldKey) + } +} + +// warnDeprecatedKeys writes a deprecation notice to stderr for each old config +// key name detected in the loaded configuration (file, env var, or CLI flag). +// Called before alias registration so IsSet reflects direct usage only. +func warnDeprecatedKeys(v *viper.Viper) { + for _, e := range compatViperEntries { + if v.IsSet(e.oldKey) { + fmt.Fprintf(os.Stderr, "ov: config key %q is deprecated, use %q instead\n", e.oldName, e.newName) + } + } +} + +// addCompatActionAliases injects new canonical action names into the handler +// map, pointing each to the handler registered under the existing internal name. +func addCompatActionAliases(handlers map[string]func(context.Context)) { + for newName, oldName := range compatActionEntries { + if h, ok := handlers[oldName]; ok { + handlers[newName] = h + } + } +} diff --git a/oviewer/keybind.go b/oviewer/keybind.go index c1625ce6..e66c59bc 100644 --- a/oviewer/keybind.go +++ b/oviewer/keybind.go @@ -145,7 +145,7 @@ const ( // handlers returns a map of the action's handlers. func (root *Root) handlers() map[string]func(context.Context) { - return map[string]func(context.Context){ + h := map[string]func(context.Context){ // General actionExit: root.Quit, actionCancel: root.Cancel, @@ -273,6 +273,8 @@ func (root *Root) handlers() map[string]func(context.Context) { inputCopy: root.CopySelect, inputPaste: root.Paste, } + addCompatActionAliases(h) + return h } // KeyBind represents a mapping from action names to their associated key sequences. diff --git a/oviewer/keybind_test.go b/oviewer/keybind_test.go index a131a997..b86d92da 100644 --- a/oviewer/keybind_test.go +++ b/oviewer/keybind_test.go @@ -31,8 +31,13 @@ func Test_defaultKeyBinds(t *testing.T) { } hm := root.handlers() kb := DefaultKeyBinds() - if len(hm) != len(kb) { - t.Errorf("number of KeyBind = %v, want %v", len(hm), len(kb)) + // handlers() may contain more entries than DefaultKeyBinds (e.g., compat + // aliases have handlers but no default key assignment), so only check that + // every default binding has a corresponding handler. + for action := range kb { + if _, ok := hm[action]; !ok { + t.Errorf("action %q in DefaultKeyBinds has no handler", action) + } } }) }