Class: Pvectl::EditorSession

Inherits:
Object
  • Object
show all
Defined in:
lib/pvectl/editor_session.rb,
sig/pvectl/editor_session.rbs

Overview

Manages the editor lifecycle for interactive config editing.

Creates a temporary file with content, opens it in an editor, reads the result, and supports a retry loop with error injection when validation fails.

Examples:

Production usage with system editor

session = EditorSession.new
result = session.edit("cpu:\n  cores: 4\n")

Testing with injected editor

editor = ->(path) { File.write(path, "modified") }
session = EditorSession.new(editor: editor)
result = session.edit("original")

With validator

validator = ->(content) { content.include?("bad") ? ["Error: bad value"] : [] }
session = EditorSession.new(editor: editor, validator: validator)
result = session.edit("original")

Defined Under Namespace

Modules: _Callable

Constant Summary collapse

ERROR_SEPARATOR =

Returns:

  • (String)
"# -----------------------------------------------"

Instance Method Summary collapse

Constructor Details

#initialize(editor: nil, validator: nil) ⇒ EditorSession

Creates a new EditorSession.

Parameters:

  • editor (#call, nil) (defaults to: nil)

    callable that receives a file path and opens it for editing. Defaults to #system_editor which uses $EDITOR/$VISUAL/vi.

  • validator (#call, nil) (defaults to: nil)

    callable that receives edited content and returns an array of error strings. Empty array means valid.

  • editor: (_Callable, nil) (defaults to: nil)
  • validator: (_Callable, nil) (defaults to: nil)


35
36
37
38
# File 'lib/pvectl/editor_session.rb', line 35

def initialize(editor: nil, validator: nil)
  @editor = editor || method(:system_editor)
  @validator = validator
end

Instance Method Details

#build_error_block(errors) ⇒ String

Builds an error block string from error messages.

Parameters:

  • errors (Array<String>)

    error messages

Returns:

  • (String)

    formatted error block with separators



127
128
129
130
131
132
133
# File 'lib/pvectl/editor_session.rb', line 127

def build_error_block(errors)
  lines = [ERROR_SEPARATOR]
  errors.each { |error| lines << "# ERROR: #{error}" }
  lines << "# Please fix the error above or save empty file to cancel."
  lines << ERROR_SEPARATOR
  "#{lines.join("\n")}\n"
end

#cancelled?(content, original_content) ⇒ Boolean

Detects whether the user cancelled editing.

Parameters:

  • content (String)

    current file content

  • original_content (String)

    the original content before editing

Returns:

  • (Boolean)

    true if editing was cancelled



87
88
89
# File 'lib/pvectl/editor_session.rb', line 87

def cancelled?(content, original_content)
  content.empty? || content == original_content
end

#default_editorString?

Returns the default editor fallback. Tries vi first, then nano. Returns nil if neither is found.

Returns:

  • (String, nil)

    the default editor command



150
151
152
153
154
155
# File 'lib/pvectl/editor_session.rb', line 150

def default_editor
  %w[vi nano].each do |cmd|
    return cmd if system("which", cmd, out: File::NULL, err: File::NULL)
  end
  nil
end

#edit(original_content) ⇒ String?

Opens content in an editor and returns the edited result.

Creates a temp file, invokes the editor, and reads back the content. Supports validation with retry loop and error injection.

Parameters:

  • original_content (String)

    the initial content to edit

Returns:

  • (String, nil)

    edited content, or nil if cancelled

Raises:

  • (RuntimeError)

    if no editor is found (system editor mode)



48
49
50
51
52
53
54
55
56
57
58
# File 'lib/pvectl/editor_session.rb', line 48

def edit(original_content)
  tempfile = Tempfile.new(["pvectl-edit-", ".yaml"])
  tempfile.write(original_content)
  tempfile.flush
  path = tempfile.path

  edit_loop(path, original_content)
ensure
  tempfile&.close
  tempfile&.unlink
end

#edit_loop(path, original_content) ⇒ String?

Runs the edit-validate-retry loop.

Parameters:

  • path (String)

    path to the temporary file

  • original_content (String)

    the original content for cancellation detection

Returns:

  • (String, nil)

    edited content, or nil if cancelled



67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/pvectl/editor_session.rb', line 67

def edit_loop(path, original_content)
  loop do
    @editor.call(path)
    content = File.read(path)

    return nil if cancelled?(content, original_content)
    return content unless @validator

    errors = @validator.call(content)
    return content if errors.empty?

    inject_errors(path, content, errors)
  end
end

#inject_errors(path, content, errors) ⇒ void

This method returns an undefined value.

Injects error comments at the top of the file for the retry loop. Strips any previous error block before injecting new ones.

Parameters:

  • path (String)

    path to the temporary file

  • content (String)

    current file content (may contain previous error block)

  • errors (Array<String>)

    error messages to inject



97
98
99
100
101
# File 'lib/pvectl/editor_session.rb', line 97

def inject_errors(path, content, errors)
  clean_content = strip_error_block(content)
  error_block = build_error_block(errors)
  File.write(path, "#{error_block}#{clean_content}")
end

#strip_error_block(content) ⇒ String

Strips a previously injected error block from content.

Parameters:

  • content (String)

    content potentially containing an error block

Returns:

  • (String)

    content without the error block



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/pvectl/editor_session.rb', line 107

def strip_error_block(content)
  lines = content.lines
  return content unless lines.first&.strip == ERROR_SEPARATOR

  # Find the end of the error block (second separator line)
  separator_count = 0
  end_index = lines.index do |line|
    separator_count += 1 if line.strip == ERROR_SEPARATOR
    separator_count >= 2
  end

  return content unless end_index

  lines[(end_index + 1)..].join
end

#system_editor(path) ⇒ void

This method returns an undefined value.

Opens a file in the system editor.

Parameters:

  • path (String)

    path to the file to edit

Raises:

  • (RuntimeError)

    if no editor is configured



139
140
141
142
143
144
# File 'lib/pvectl/editor_session.rb', line 139

def system_editor(path)
  editor = ENV["EDITOR"] || ENV["VISUAL"] || default_editor
  raise "No editor found. Set $EDITOR or $VISUAL environment variable." unless editor

  system(editor, path)
end