lib/iex_history2.ex

#
# MIT License
#
# Copyright (c) 2021 Matthew Evans
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#

defmodule IExHistory2 do
  @moduledoc """
  Saves shell history and variable bindings between shell sessions.

  Allows the user to display history, and re-issue historic commands, made much easier since the variable bindings are saved.

  IExHistory2 can be enabled in `~/.iex.exs` for example:

      Code.append_path("~/github/iex_history2/_build/dev/lib/iex_history2/ebin")
      IExHistory2.initialize(history_limit: 200, scope: :local, show_date: true, colors: [index: :red])

  The application can, of course, be added as a dependency to mix.exs

  ## Functions
    
      iex> hl()             - list the entire history.
      
      iex> hl(val)          - list `val` entries from the start if val is positive, or from the end if negative.
      
      iex> hl(start, stop)  - list entries between `start` and `stop`.
      
      iex> hs(string)       - list entries that match all or part of the query string.

      iex> hsi(string)      - case insensitive list entries that match all or part of the query string.

      iex> hsa(string)      - closest match list of entries, e.g "acr.to_str" == "Macro.to_string"
      
      iex> hx(pos)          - execute the expression at position `pos`.
      
      iex> hc(pos)          - copy the expression at position pos to the shell.
      
      iex> he(pos)          - edit the expression in a text editor.
      
      iex> hb()             - show the current bindings.
      
      iex> hi()             - summary

  NOTE: To use `he/1` the environment variable `VISUAL` must be set to point to the editor:
  
      export VISUAL="vim"

  ## Special Functions

      iex> IExHistory2.add_binding(var, val)
      
      iex> IExHistory2.get_binding(var)
      
      iex> IExHistory2.clear_history()
      
      iex> IExHistory2.clear_bindings()
          
  The functions `IExHistory2.add_binding/2` and `IExHistory2.get_binding/1` allows variables
  to be set in a module that is invoked in the shell to be accessible in the shell.
          
  ## Navigation
          
  The application uses a different set of keys for navigation, and attempts to present multi-line 
  terms and other items as a single line:
  
      ctrl^u    - move up through history.
      
      ctrl^k    - move down through history.
      
      ctrl^e    - allows the currently displayed item to be modified.
      
      ctrl^w    - opens the currently displayed item in an editor.
              
      ctrl^[    - reset navigation, returns to the prompt.
              
  ## Configuration
    
    The following options can be set:

      [
        scope: :local,
        history_limit: :infinity,
        hide_history_commands: true,
        prepend_identifiers: true,
        command_display_width: int,
        save_invalid_results: false,
        key_buffer_history: true,
        show_date: true,
        save_bindings: true,
        colors: [
          index: :red,
          date: :green,
          command: :yellow,
          label: :red,
          variable: :green
        ]
      ]
        
    `:hide_history_commands ` This will prevent all calls to `IExHistory2.*` from been saved.

    `:save_invalid_results ` If set to false, the default, commands that were evaluated incorrectly will not be saved.

    `:key_buffer_history ` If set to true will allow the user to scroll up (ctrl+u) or down (ctrl+k) through history.
    Unlike the standard up/down arrow history this is command based not line based. So pasting of a large term or source code will only require 1 up or down key.
    
    This mechanism also saves commands that were not properly evaluated; however there is a buffer limit of 150 lines, although this can be changed by updating
    `@history_buffer_size` in `events_server.ex`. This will also not duplicate back to back identical commands.

    `:prepend_identifiers ` If this is enabled it will prepend identifiers when a call to `x = hx(val)` is issued.

    For example:

      enabled:
          iex> time = Time.utc_now().second
          14
          iex> new_time = hx(1)
          22

          iex> new_time
          22                  # New time is assigned to variable time
          iex> time
          13                  # However, the original date variable is unchanged

          iex> hl()
          1: 2021-09-01 17:13:13: time = Time.utc_now().second
          2: 2021-09-01 17:13:22: new_time =  time = Time.utc_now().second    # We see the binding to new_time

        disabled:
          iex> time = Time.utc_now().second
          43
          iex> new_time = hx(1)
          50

          iex> new_time       # New time is assigned to variable time
          50
          iex> time
          50                  # However, this time the original time variable has also changed

          iex> hl()
          1: 2021-09-01 17:17:43: time = Time.utc_now().second
          2: 2021-09-01 17:17:50: time = Time.utc_now().second      # We do not see the binding to new_time


    `scope` can be one of `:local, :global `or a `node name`

    If `scope` is `:local` (the default) history will be active on all shells, even those that are remotely connected, but the history for each shell will be unique

    If `scope` is `node()` (e.g. `:mgr@localhost`) history will only be active on that shell

    If `scope` is `:global` history will be shared between all shells. However the saving of variable bindings will be disabled along with the date/time in history

    Furthermore, if a `scope` of `:global` is selected following kernel option must be set, either directly as VM options or via an environment variable:

      export ERL_AFLAGS="-kernel shell_history enabled"

      --erl "-kernel shell_history enabled"

  """

  @version "5.0"
  @module_name String.trim_leading(Atom.to_string(__MODULE__), "Elixir.")
  @exec_name String.trim_leading(Atom.to_string(__MODULE__) <> ".hx", "Elixir.")

  @shell_imports [hl: 0, hl: 1, hl: 2, hs: 1, hsi: 1, hsa: 1,
                  hsa: 2, hc: 1, hx: 1, hb: 0, hi: 0, he: 1]
  @excluded_history_functions [".h(", ".x(", ".c("]
  @excluded_history_imports ["hc(", "hl(", "hs(", "hsi(", "hsa(", "hx(", "hb(", "hi(", "he(",
                             "hc ", "hl", "hs ", "hsi ", "hsa ", "hx ", "hb ", "hi ", "he "]
  @exclude_from_history for f <- @excluded_history_functions, do: @module_name <> f

  @default_width 150
  @default_colors [index: :red, date: :green, command: :yellow, label: :red, variable: :green, binding: :cyan]
  @default_config [
    scope: :local,
    history_limit: :infinity,
    hide_history_commands: true,
    prepend_identifiers: true,
    show_date: true,
    save_bindings: true,
    command_display_width: @default_width,
    import: true,
    save_invalid_results: false,
    key_buffer_history: true,
    colors: @default_colors
  ]

  @doc """
    Initializes the IExHistory2 app. Takes the following parameters:

      [
        scope: :local,
        history_limit: :infinity,
        hide_history_commands: true,
        prepend_identifiers: true,
        key_buffer_history: true,
        command_display_width: :int,
        save_invalid_results: false,
        show_date: true,
        import: true,
        save_bindings: true,
        colors: [
          index: :red,
          date: :green,
          command: :yellow,
          label: :red,
          variable: :green
        ]
      ]

    Alternatively a filename can be given that was saved with `IExHistory2.save_config()`

    `scope` can be one of `:local, :global` or a `node()` name
  """
  def initialize(config_or_filename \\ []) do
    config = do_load_config(config_or_filename)

    if history_configured?(config) && not is_enabled?() do
      :dbg.stop()
      new_config = init_save_config(config)
      inject_command("IEx.configure(colors: [syntax_colors: [atom: :black]])")

      if Keyword.get(new_config, :import),
        do: inject_command("import IExHistory2, only: #{inspect(@shell_imports)}")

      IExHistory2.Events.initialize(new_config)
      |> IExHistory2.Bindings.initialize()
      |> set_enabled()
      |> present_welcome()
    else
      if is_enabled?(), do: :history_already_enabled, else: :history_disabled
    end
  end

  @doc false
  def alias(name) when is_atom(name) do
    if Process.get(:history_alias) == nil do
      string_name = Atom.to_string(name) |> String.replace("Elixir.", "")
      inject_command_all_servers("alias(#{__MODULE__}, as: #{string_name})")
      excluded = for fun <- @excluded_history_functions, do: string_name <> fun
      base_name = string_name <> "."
      Process.put(:history_alias, base_name)
      IExHistory2.Events.send_message({:module_alias, base_name})
      ## TODO: Find a better way
      :persistent_term.put(:history_aliases, excluded ++ :persistent_term.get(:history_aliases, []))
    end
  end

  @doc """
  Displays the current configuration.
  """
  def configuration(), do: Process.get(:history_config, [])

  @doc """
  Displays the default configuration.
  """
  def default_config(), do: @default_config

  @doc """
  Displays the current state:

      IExHistory2 version 2.0 is enabled:
        Current history is 199 commands in size.
        Current bindings are 153 variables in size.
  """
  def state() do
    IO.puts("IExHistory2 version #{IO.ANSI.red()}#{@version}#{IO.ANSI.white()} is enabled:")
    IO.puts("  #{IExHistory2.Events.state()}.")
    IO.puts("  #{IExHistory2.Bindings.state()}.")
  end

  @doc """
  Displays the entire history.
  """
  def hl() do
    is_enabled!()

    query_search(fn ->  IExHistory2.Events.get_history() end)
  end

  @doc """
  Displays the entire history from the most recent entry back (negative number),
  or from the oldest entry forward (positive number)
  """
  @spec hl(integer()) :: nil
  def hl(val) when val < 0 do
    is_enabled!()

    query_search(fn ->  IExHistory2.Events.get_history_item(val) end)
  end

  def hl(val) when val > 0 do
    is_enabled!()

    query_search(fn ->  IExHistory2.Events.get_history_items(1, val) end)
  end

  @doc """
  Specify a range, the atoms :start and :stop can also be used.
  """
  @spec hl(integer(), integer()) :: nil
  def hl(start, stop) do
    is_enabled!()

    query_search(fn ->  IExHistory2.Events.get_history_items(start, stop) end)
  end

  @doc """
  Returns the list of expressions where all or part of the string matches.

  The original expression does not need to be a string.
  """
  @spec hs(String.t()) :: nil
  def hs(match) do
    is_enabled!()

    query_search(fn ->  IExHistory2.Events.search_history_items(match, :exact) end)
  end

  @doc """
  A case insensitive search the list of expressions where all or part of the string matches.

  The original expression does not need to be a string.
  """
  @spec hsi(String.t()) :: nil
  def hsi(match) do
    is_enabled!()

    query_search(fn ->  IExHistory2.Events.search_history_items(match, :ignore_case) end)
  end
  
  @doc """
  Like `hsa/1` a case insensitive search, but also adds a closeness element to the search.

  It uses a combination of Myers Difference and Jaro Distance to get close to a match. The
  estimated closeness is indicated in the result with a default range of > 80%.
  This can be set by the user.
  
  For large histories this command may take several seconds.
  
  The original expression does not need to be a string.
  """
  @spec hsa(String.t(), integer()) :: nil
  def hsa(match, closeness \\ 80) do
    is_enabled!()

    query_search(fn ->  IExHistory2.Events.search_history_items(match, :approximate, closeness) end)
  end
  
  @doc """
  Invokes the command at index 'i'.
  """
  @spec hx(integer()) :: any()
  def hx(i) do
    is_enabled!()

    try do
      IExHistory2.Events.execute_history_item(i)
    catch
      _, {:badmatch, nil} -> {:error, :not_found}
      :error, %CompileError{description: descr} -> {:error, descr}
      error, rsn -> {error, rsn}
    end
  end

  @doc """
  Copies the command at index 'i' and pastes it to the shell.
  """
  @spec hc(integer()) :: any()
  def hc(i) do
    is_enabled!()
    
    query_search(fn -> IExHistory2.Events.copy_paste_history_item(i) end)
  end

  @spec he(integer()) :: any()
  def he(i) do
    is_enabled!()

     query_search(fn ->  IExHistory2.Events.edit_history_item(i) end)
  end

  @doc """
  Show the variable bindings.
  """
  def hb(),
    do: IExHistory2.Bindings.display_bindings()

  @doc """
  Show history information summary.
  """
  def hi(),
    do: state()

  ###
  # Backwards compatibility
  ###
  @doc false
  def h(), do: hl()
  @doc false
  def h(val), do: hl(val)
  @doc false
  def h(start, stop), do: hl(start, stop)
  @doc false
  def c(val), do: hc(val)
  @doc false
  def x(val), do: hx(val)
  @doc false  
  def save_binding(val), do: add_binding(val)
  @doc false
  def save_binding(var, val), do: add_binding(var, val)
    
  @doc """
    Clears the history and bindings. If `scope` is `:global`
    the IEx session needs restarting for the changes to take effect.
  """
  def clear() do
    IExHistory2.Events.clear()
    IExHistory2.Bindings.clear()

    if IExHistory2.configuration(:scope, :local) == :global,
      do: IO.puts("\n#{IO.ANSI.green()}Please restart your shell session for the changes to take effect")

    :ok
  end

  @doc """
  Clears the history only. If `scope` is `:global`
  the IEx session needs restarting for the changes to take effect. If a value is passed it will clear that many history
  entries from start, otherwise the entire history is cleared.
  """
  def clear_history(val \\ :all) do
    IExHistory2.Events.clear_history(val)

    if IExHistory2.configuration(:scope, :local) == :global && val == :all,
      do: IO.puts("\n#{IO.ANSI.green()}Please restart your shell session for the changes to take effect")

    :ok
  end

  @doc """
  Clears the bindings.
  """
  def clear_bindings() do
    IExHistory2.Bindings.clear()
    :ok
  end

  @doc """
  Clears the history and bindings then stops the service. If `scope` is ` :global` the IEx session needs restarting for the changes to take effect.
  """
  def stop_clear() do
    IExHistory2.Events.stop_clear()
    IExHistory2.Bindings.stop_clear()

    if IExHistory2.configuration(:scope, :local) == :global,
      do: IO.puts("\n#{IO.ANSI.green()}Please restart your shell session for the changes to take effect")

    :ok
  end

  @doc """
  Returns `true` or `false` depending on if history is enabled.
  """
  def is_enabled?() do
    Process.get(:history_is_enabled, false)
  end

  @doc """
  Returns the current shell bindings.
  """
  def get_bindings() do
    IExHistory2.Bindings.get_bindings()
  end

  @doc """
  This helper function can be used when testing code (for example a module pasted into the shell).
  It allows a variable that is set in the shell to be available in a module under test. For example:
  
      defmodule VarTest do
        def get_me(val) do
          if IExHistory2.get_binding(:path_to_use) == :path1 do
            val + 100
          else
            val + 200
          end
        end     
      end
      
      iex> path_to_use = :path1
      :path1
      iex> VarTest.get_me(50)
      150
      iex> path_to_use = :path2
      :path2
      iex> VarTest.get_me(50)
      250
  The variable can be represented as an atom or string.      
  """
  @spec get_binding(atom() | String.t()) :: any()
  def get_binding(var) when is_bitstring(var) do
    get_binding(String.to_atom(var))
  end

  def get_binding(var) do
    try do
      IExHistory2.Bindings.get_binding(var)
    rescue
    _ -> raise("undefined variable #{var}")  
    end
  end
  
  @doc """
  Experimental. Same as `get_binding/2`, but `name` is the registered name of a shell pid.
  
  See `register/1`
  """
  @spec get_binding(atom() | String.t(), atom()) :: any()  
  def get_binding(var, name) when is_bitstring(var) do
    get_binding(String.to_atom(var), name)
  end
  
  def get_binding(var, name) do
    try do
      IExHistory2.Bindings.get_binding(var, name)
    rescue
    _ -> raise("undefined variable #{var}")  
    end
  end
  
  @doc """
  This helper function can be used when testing code (for example a module pasted into the shell).
  It allows a variable to be set that will become available in the shell. For example:
  
      defmodule VarTest do
        def set_me(var) do
          var = var * 2
          IExHistory2.add_binding(:test_var, var)
          var + 100
        end
      end
  
      iex> VarTest.set_me(7)
      
      iex> test_var
      14
        
  The variable can be represented as an atom or string.      
  """
  @spec add_binding(atom() | String.t(), any()) :: :ok
  def add_binding(var, value) do
    inject_command("#{var} = #{inspect(value, limit: :infinity, printable_limit: :infinity)}")
    :ok
  end
  
  @doc """
  Experimental. Same as `add_binding/2`, but `name` is the registered name of a shell pid.
  
  See `register/1`
  """
  @spec add_binding(atom() | String.t(), any(), atom()) :: :ok
  def add_binding(var, value, name) do
    inject_command("#{var} = #{inspect(value, limit: :infinity, printable_limit: :infinity)}", name)
    :ok
  end

  @doc false
  def add_binding(value) do
    inject_command("#{inspect(value, limit: :infinity, printable_limit: :infinity)}")
    :ok
  end
  
  @doc """
  Registers the shell under the name provided.
  """
  @spec register(atom()) :: :ok
  def register(name) do
    Process.register(self(), name)  
  end
  
  @doc false
  def eval_on_shell(value, name) do
    inject_command("#{inspect(value, limit: :infinity, printable_limit: :infinity)}", name)
    :ok
  end
    
  @doc false
  def eval_on_shell(var, value, name) do
    inject_command("#{var} = #{inspect(value, limit: :infinity, printable_limit: :infinity)}", name)
    :ok
  end
  
  @doc """
  Unbinds a variable or list of variables (specify variables as atoms, e.g. foo becomes :foo).
  """
  def unbind(vars) when is_list(vars), do: IExHistory2.Bindings.unbind(vars)
  def unbind(var), do: unbind([var])

  @doc """
  Saves the current configuration to file.
  """
  def save_config(filename) do
    data = :io_lib.format("~p.", [configuration()]) |> List.flatten()
    :file.write_file(filename, data)
  end

  @doc """
  Loads the current configuration to file `IExHistory2.save_config()`.

  NOTE: Not all options can be set during run-time. Instead pass the filename as a single argument to `IExHistory2.initialize()`
  """
  def load_config(filename) do
    config = do_load_config(filename)
    Process.put(:history_config, config)
    config
  end

  @doc """
    Allows the following options to be changed, but not saved:
      :show_date
      :history_limit
      :hide_history_commands,
      :prepend_identifiers,
      :save_bindings,
      :command_display_width,
      :save_invalid_results,
      :key_buffer_history,
      :colors

  Examples:
      IExHistory2.configure(:colors, [index: :blue])
      IExHistory2.configure(:prepend_identifiers, true)
  """
  @spec configure(atom(), any) :: atom
  def configure(kry, val)

  def configure(:show_date, value) when value in [true, false] do
    new_config = List.keyreplace(configuration(), :show_date, 0, {:show_date, value})
    Process.put(:history_config, new_config)
    configuration()
  end

  def configure(:command_display_width, value) when is_integer(value) do
    new_config = List.keyreplace(configuration(), :command_display_width, 0, {:command_display_width, value})
    Process.put(:history_config, new_config)
    configuration()
  end

  def configure(:hide_history_commands, value) when value in [true, false] do
    new_config = List.keyreplace(configuration(), :hide_history_commands, 0, {:hide_history_commands, value})
    IExHistory2.Events.send_message({:hide_history_commands, value})
    Process.put(:history_config, new_config)
    configuration()
  end

  def configure(:key_buffer_history, value) when value in [true, false] do
    new_config = List.keyreplace(configuration(), :key_buffer_history, 0, {:key_buffer_history, value})
    IExHistory2.Events.send_message({:key_buffer_history, value})
    Process.put(:history_config, new_config)
    configuration()
  end

  def configure(:save_invalid_results, value) when value in [true, false] do
    new_config = List.keyreplace(configuration(), :save_invalid_results, 0, {:save_invalid_results, value})
    IExHistory2.Events.send_message({:save_invalid_results, value})
    Process.put(:history_config, new_config)
    configuration()
  end

  def configure(:prepend_identifiers, value) when value in [true, false] do
    new_config = List.keyreplace(configuration(), :prepend_identifiers, 0, {:prepend_identifiers, value})
    IExHistory2.Events.send_message({:prepend_identifiers, value})
    Process.put(:history_config, new_config)
    configuration()
  end

  def configure(:history_limit, value) when is_integer(value) or value == :infinity do
    new_config = List.keyreplace(configuration(), :history_limit, 0, {:history_limit, value})
    new_value = if value == :infinity, do: IExHistory2.Events.infinity_limit(), else: value
    IExHistory2.Events.send_message({:new_history_limit, new_value})
    Process.put(:history_config, new_config)
    configuration()
  end

  def configure(:save_bindings, value) when value in [true, false] do
    if configuration(:scope, :local) != :global do
      current_value = configuration(:save_bindings, true)
      new_config = List.keyreplace(configuration(), :save_bindings, 0, {:save_bindings, value})

      if current_value == true,
        do: IExHistory2.Bindings.stop_clear(),
        else: IExHistory2.Bindings.initialize(new_config)

      Process.put(:history_config, new_config)
      configuration()
    else
      {:error, :scope_is_global}
    end
  end

  def configure(:colors, keyword_list) do
    new_colors = Keyword.merge(configuration(:colors, []), keyword_list)
    new_config = List.keyreplace(configuration(), :colors, 0, {:colors, new_colors})
    Process.put(:history_config, new_config)
    configuration()
  end

  @doc false
  def get_color_code(for), do: Kernel.apply(IO.ANSI, configuration(:colors, @default_colors)[for], [])

  @doc false
  def get_log_path() do
    filename = :filename.basedir(:user_cache, ~c"erlang-history") |> to_string()
    File.mkdir_p!(filename)
    filename
  end

  @doc false
  def my_real_node(), do: :erlang.node(Process.group_leader())

  @doc false
  def module_name(), do: @module_name

  @doc false
  def exec_name(), do: @exec_name

  @doc false
  def exclude_from_history() do
    aliases = :persistent_term.get(:history_aliases, [])
    @exclude_from_history ++ @excluded_history_imports ++ aliases
  end

  @doc false
  def configuration(item, default), do: Keyword.get(configuration(), item, default)

  @doc false
  def persistence_mode(:local) do
    my_node = my_real_node()
    {:ok, true, :local, my_node}
  end

  @doc false
  def persistence_mode(:global), do: {:ok, true, :global, :no_node}

  @doc false
  def persistence_mode(node) when is_atom(node) do
    my_node = my_real_node()

    if my_node == node,
      do: {:ok, true, :local, my_node},
      else: {:ok, false, :no_label, :no_node}
  end

  @doc false
  def persistence_mode(node) when is_binary(node) do
    persistence_mode(String.to_atom(node))
  end

  @doc false
  def persistence_mode(_), do: {:ok, false, :no_label, :no_node}

  @doc false
  def inject_command(command, name \\ nil), do: IExHistory2.Bindings.inject_command(command, name)

  defp inject_command_all_servers(command) do
    Enum.each(
      Process.list(),
      fn pid ->
        server = :group.whereis_shell()

        if not is_nil(server),
          do: send(pid, {:eval, server, command, 1, {"", :other}})
      end
    )
  end

  defp query_search(fun) do
    try do
      fun.()
    catch
      _, _ -> {:error, :not_found}
    end
  end
  
  defp is_enabled!() do
    if not is_enabled?(),
      do: raise(%ArgumentError{message: "IExHistory2 is not enabled"})
  end

  defp do_load_config(filename) when is_binary(filename) do
    {:ok, [config]} = :file.consult(filename)
    config
  end

  defp do_load_config(config), do: config

  defp init_save_config(config) do
    infinity_limit = IExHistory2.Events.infinity_limit()
    colors = Keyword.get(config, :colors, @default_colors)
    new_colors = Enum.map(@default_colors, fn {key, default} -> {key, Keyword.get(colors, key, default)} end)
    config = Keyword.delete(config, :colors)

    new_config =
      Enum.map(
        @default_config,
        fn
          {:colors, _} -> {:colors, Keyword.get(config, :colors, new_colors)}
          {:limit, current} when current > infinity_limit -> {:limit, Keyword.get(config, :limit, infinity_limit)}
          {key, default} -> {key, Keyword.get(config, key, default)}
        end
      )
            
    if Keyword.get(new_config, :scope, :local) == :global do
      newer_config = List.keyreplace(new_config, :save_bindings, 0, {:save_bindings, false})
      Process.put(:history_config, newer_config)
      newer_config
    else
      Process.put(:history_config, new_config)
      new_config
    end
  end

  defp history_configured?(config) do
    scope = Keyword.get(config, :scope, :local)

    if IExHistory2.Events.does_current_scope_match?(scope) do
      my_node = my_real_node()

      if my_node == scope || scope in [:global, :local],
        do: true,
        else: false
    else
      false
    end
  end

  defp present_welcome(:not_ok), do: :ok

  defp present_welcome(_), do: inject_command("IExHistory2.state(); IEx.configure(colors: [syntax_colors: [atom: :cyan]])")

  defp set_enabled(config) do
    Process.put(:history_is_enabled, true)
    config
  end
end