├── .credo.exs ├── .formatter.exs ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── .tool-versions ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── docs └── console.png ├── lib ├── pane.ex └── pane │ ├── page.ex │ └── viewer.ex ├── mix.exs ├── mix.lock └── test ├── pane ├── page_test.exs └── viewer_test.exs ├── pane_test.exs └── test_helper.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any config using `mix credo -C `. If no config name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: [ 25 | "lib/", 26 | "src/", 27 | "test/", 28 | "web/", 29 | "apps/*/lib/", 30 | "apps/*/src/", 31 | "apps/*/test/", 32 | "apps/*/web/" 33 | ], 34 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] 35 | }, 36 | # 37 | # Load and configure plugins here: 38 | # 39 | plugins: [], 40 | # 41 | # If you create your own checks, you must specify the source files for 42 | # them here, so they can be loaded by Credo before running the analysis. 43 | # 44 | requires: [], 45 | # 46 | # If you want to enforce a style guide and need a more traditional linting 47 | # experience, you can change `strict` to `true` below: 48 | # 49 | strict: false, 50 | # 51 | # To modify the timeout for parsing files, change this value: 52 | # 53 | parse_timeout: 5000, 54 | # 55 | # If you want to use uncolored output by default, you can change `color` 56 | # to `false` below: 57 | # 58 | color: true, 59 | # 60 | # You can customize the parameters of any check by adding a second element 61 | # to the tuple. 62 | # 63 | # To disable a check put `false` as second element: 64 | # 65 | # {Credo.Check.Design.DuplicatedCode, false} 66 | # 67 | checks: [ 68 | # 69 | ## Consistency Checks 70 | # 71 | {Credo.Check.Consistency.ExceptionNames, []}, 72 | {Credo.Check.Consistency.LineEndings, []}, 73 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 74 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 75 | {Credo.Check.Consistency.SpaceInParentheses, []}, 76 | {Credo.Check.Consistency.TabsOrSpaces, []}, 77 | 78 | # 79 | ## Design Checks 80 | # 81 | # You can customize the priority of any check 82 | # Priority values are: `low, normal, high, higher` 83 | # 84 | {Credo.Check.Design.AliasUsage, 85 | [ 86 | priority: :low, 87 | if_nested_deeper_than: 2, 88 | if_called_more_often_than: 0 89 | ]}, 90 | # You can also customize the exit_status of each check. 91 | # If you don't want TODO comments to cause `mix credo` to fail, just 92 | # set this value to 0 (zero). 93 | # 94 | {Credo.Check.Design.TagTODO, [exit_status: 2]}, 95 | {Credo.Check.Design.TagFIXME, []}, 96 | 97 | # 98 | ## Readability Checks 99 | # 100 | {Credo.Check.Readability.AliasOrder, []}, 101 | {Credo.Check.Readability.FunctionNames, []}, 102 | {Credo.Check.Readability.LargeNumbers, []}, 103 | {Credo.Check.Readability.MaxLineLength, 104 | [priority: :low, max_length: 120]}, 105 | {Credo.Check.Readability.ModuleAttributeNames, []}, 106 | {Credo.Check.Readability.ModuleDoc, []}, 107 | {Credo.Check.Readability.ModuleNames, []}, 108 | {Credo.Check.Readability.ParenthesesInCondition, []}, 109 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, 110 | {Credo.Check.Readability.PredicateFunctionNames, []}, 111 | {Credo.Check.Readability.PreferImplicitTry, []}, 112 | {Credo.Check.Readability.RedundantBlankLines, []}, 113 | {Credo.Check.Readability.Semicolons, []}, 114 | {Credo.Check.Readability.SpaceAfterCommas, []}, 115 | {Credo.Check.Readability.StringSigils, []}, 116 | {Credo.Check.Readability.TrailingBlankLine, []}, 117 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 118 | {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, 119 | {Credo.Check.Readability.VariableNames, []}, 120 | 121 | # 122 | ## Refactoring Opportunities 123 | # 124 | {Credo.Check.Refactor.CondStatements, []}, 125 | {Credo.Check.Refactor.CyclomaticComplexity, []}, 126 | {Credo.Check.Refactor.FunctionArity, []}, 127 | {Credo.Check.Refactor.LongQuoteBlocks, []}, 128 | {Credo.Check.Refactor.MapInto, false}, 129 | {Credo.Check.Refactor.MatchInCondition, []}, 130 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 131 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 132 | {Credo.Check.Refactor.Nesting, []}, 133 | {Credo.Check.Refactor.UnlessWithElse, []}, 134 | {Credo.Check.Refactor.WithClauses, []}, 135 | 136 | # 137 | ## Warnings 138 | # 139 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 140 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 141 | {Credo.Check.Warning.IExPry, []}, 142 | {Credo.Check.Warning.IoInspect, []}, 143 | {Credo.Check.Warning.LazyLogging, false}, 144 | {Credo.Check.Warning.MixEnv, false}, 145 | {Credo.Check.Warning.OperationOnSameValues, []}, 146 | {Credo.Check.Warning.OperationWithConstantResult, []}, 147 | {Credo.Check.Warning.RaiseInsideRescue, []}, 148 | {Credo.Check.Warning.UnusedEnumOperation, []}, 149 | {Credo.Check.Warning.UnusedFileOperation, []}, 150 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 151 | {Credo.Check.Warning.UnusedListOperation, []}, 152 | {Credo.Check.Warning.UnusedPathOperation, []}, 153 | {Credo.Check.Warning.UnusedRegexOperation, []}, 154 | {Credo.Check.Warning.UnusedStringOperation, []}, 155 | {Credo.Check.Warning.UnusedTupleOperation, []}, 156 | {Credo.Check.Warning.UnsafeExec, []}, 157 | 158 | # 159 | # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) 160 | 161 | # 162 | # Controversial and experimental checks (opt-in, just replace `false` with `[]`) 163 | # 164 | {Credo.Check.Readability.StrictModuleLayout, false}, 165 | {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, 166 | {Credo.Check.Consistency.UnusedVariableNames, false}, 167 | {Credo.Check.Design.DuplicatedCode, false}, 168 | {Credo.Check.Readability.AliasAs, false}, 169 | {Credo.Check.Readability.MultiAlias, false}, 170 | {Credo.Check.Readability.Specs, false}, 171 | {Credo.Check.Readability.SinglePipe, false}, 172 | {Credo.Check.Readability.WithCustomTaggedTuple, false}, 173 | {Credo.Check.Refactor.ABCSize, false}, 174 | {Credo.Check.Refactor.AppendSingleItem, false}, 175 | {Credo.Check.Refactor.DoubleBooleanNegation, false}, 176 | {Credo.Check.Refactor.ModuleDependencies, false}, 177 | {Credo.Check.Refactor.NegatedIsNil, false}, 178 | {Credo.Check.Refactor.PipeChainStart, false}, 179 | {Credo.Check.Refactor.VariableRebinding, false}, 180 | {Credo.Check.Warning.LeakyEnvironment, false}, 181 | {Credo.Check.Warning.MapGetUnsafePass, false}, 182 | {Credo.Check.Warning.UnsafeToAtom, false} 183 | 184 | # 185 | # Custom checks can be created using `mix credo.gen.check`. 186 | # 187 | ] 188 | } 189 | ] 190 | } 191 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | line_length: 80 5 | ] 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [codedge-llc] 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | jobs: 10 | prettier: 11 | name: Prettier 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | with: 17 | ref: ${{ github.head_ref }} 18 | - name: Use Node.js 18.x 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 18.x 22 | - name: Install Prettier 23 | run: npm install --global prettier 24 | - name: Run Prettier 25 | run: prettier --check --no-error-on-unmatched-pattern "**/*.{json,md,yml,yaml}" 26 | check: 27 | name: Format/Credo 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v3 31 | - name: Set up Elixir 32 | uses: erlef/setup-beam@v1 33 | with: 34 | elixir-version: "1.18.2" 35 | otp-version: "27.2" 36 | - name: Restore dependencies cache 37 | uses: actions/cache@v3 38 | with: 39 | path: deps 40 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 41 | restore-keys: ${{ runner.os }}-mix- 42 | - name: Install dependencies 43 | run: mix deps.get 44 | - name: Run formatter 45 | run: mix format --check-formatted 46 | - name: Run Credo 47 | run: mix credo 48 | test: 49 | name: Test 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v3 53 | - name: Set up Elixir 54 | uses: erlef/setup-beam@v1 55 | with: 56 | elixir-version: "1.18.0" 57 | otp-version: "27.0.1" 58 | - name: Restore dependencies cache 59 | uses: actions/cache@v3 60 | with: 61 | path: deps 62 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 63 | restore-keys: ${{ runner.os }}-mix- 64 | - name: Install dependencies 65 | run: mix deps.get 66 | - name: Run tests 67 | env: 68 | TERM: xterm 69 | run: mix test 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | pane-*.tar 24 | 25 | # Temporary files for e.g. tests. 26 | /tmp/ 27 | 28 | # Ignore dialyzer files. 29 | /priv/plts/*.plt 30 | /priv/plts/*.plt.hash 31 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.18.2-otp-27 2 | erlang 27.2 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## v0.5.0 - 2024-08-31 9 | 10 | **Changed** 11 | 12 | - Various documentation updates ([#5](https://github.com/codedge-llc/pane/pull/5)). 13 | - Bumped minimum Elixir version to 1.13. 14 | 15 | ## v0.4.1 - 2021-02-27 16 | 17 | **Fixed** 18 | 19 | - Include `:iex` in `:extra_applications` to remove compile warning. 20 | 21 | ## v0.4.0 - 2020-09-28 22 | 23 | **Changed** 24 | 25 | - Bump minimum Elixir version to 1.5. 26 | 27 | **Fixed** 28 | 29 | - Remove deprecated compile warnings. 30 | 31 | ## v0.3.0 - 2018-01-18 32 | 33 | - Support for (f)first and (l)last page 34 | 35 | ## v0.2.0 - 2018-01-03 36 | 37 | - Prompt to enable ANSI codes if disabled 38 | 39 | ## v0.1.1 - 2017-03-16 40 | 41 | - Fix: crash on invalid key command 42 | 43 | ## v0.1.0 - 2017-02-26 44 | 45 | - Initial release 46 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2025 Codedge LLC (https://www.codedge.io/) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pane 2 | 3 | > Paginated data viewer for IEx. Written for [scribe](https://github.com/codedge-llc/scribe). 4 | > Useful for inspecting large collections and deeply nested structs. 5 | 6 | [![CI](https://github.com/codedge-llc/pane/actions/workflows/ci.yml/badge.svg)](https://github.com/codedge-llc/pane/actions/workflows/ci.yml) 7 | [![Version](https://img.shields.io/hexpm/v/pane.svg)](https://hex.pm/packages/pane) 8 | [![Total Downloads](https://img.shields.io/hexpm/dt/pane.svg)](https://hex.pm/packages/pane) 9 | [![License](https://img.shields.io/hexpm/l/pane.svg)](https://github.com/codedge-llc/pane/blob/main/LICENSE.md) 10 | [![Last Updated](https://img.shields.io/github/last-commit/codedge-llc/pane.svg)](https://github.com/codedge-llc/pane/commits/main) 11 | [![Documentation](https://img.shields.io/badge/documentation-gray)](https://hexdocs.pm/pane/) 12 | 13 | ## Installation 14 | 15 | Add `:pane` as a `mix.exs` dependency: 16 | 17 | ```elixir 18 | def deps do 19 | [ 20 | {:pane, "~> 0.5.0"} 21 | ] 22 | end 23 | ``` 24 | 25 | ## Usage 26 | 27 | iex> data = File.read!("mix.exs") # Or some other really long string 28 | iex> Pane.console(data) 29 | 30 | ![console](https://raw.githubusercontent.com/codedge-llc/pane/main/docs/console.png) 31 | 32 | ## Available Commands 33 | 34 | - `j` - Next page 35 | - `k` - Previous page 36 | - `f` - First page 37 | - `l` - Last page 38 | - `q` - Quit 39 | 40 | ## Contributing 41 | 42 | ### Testing 43 | 44 | Unit tests can be run with `mix test` or `mix coveralls.html`. 45 | 46 | ### Formatting 47 | 48 | This project uses Elixir's `mix format` and [Prettier](https://prettier.io) for formatting. 49 | Add hooks in your editor of choice to run it after a save. Be sure it respects this project's 50 | `.formatter.exs`. 51 | 52 | ### Commits 53 | 54 | Git commit subjects use the [Karma style](http://karma-runner.github.io/5.0/dev/git-commit-msg.html). 55 | 56 | ## License 57 | 58 | Copyright (c) 2017-2025 Codedge LLC (https://www.codedge.io/) 59 | 60 | This library is MIT licensed. See the [LICENSE](https://github.com/codedge-llc/pane/blob/main/LICENSE.md) for details. 61 | -------------------------------------------------------------------------------- /docs/console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codedge-llc/pane/d3d1da9b0650fa60be379c3c94ae1066f4fafd68/docs/console.png -------------------------------------------------------------------------------- /lib/pane.ex: -------------------------------------------------------------------------------- 1 | defmodule Pane do 2 | @moduledoc """ 3 | Paginated data viewer for IEx. Useful for inspecting large collections and 4 | deeply nested structs. 5 | 6 | ## Usage 7 | 8 | ``` 9 | iex> data = File.read!("mix.exs") # Or some other really long string 10 | iex> Pane.console(data) 11 | ``` 12 | 13 | ``` 14 | defmodule Pane.Mixfile do 15 | use Mix.Project 16 | 17 | @source_url "https://github.com/codedge-llc/pane" 18 | @version "0.5.0" 19 | 20 | def project do 21 | [ 22 | app: :pane, 23 | build_embedded: Mix.env() == :prod, 24 | deps: deps(), 25 | dialyzer: dialyzer(), 26 | docs: docs(), 27 | elixir: "~> 1.13", 28 | name: "Pane", 29 | 30 | [1 of 5] (j)next (k)prev (f)first (l)last (q)quit 31 | ``` 32 | 33 | ## Available Commands 34 | * `j` - Next page 35 | * `k` - Previous page 36 | * `f` - First page 37 | * `l` - Last page 38 | * `q` - Quit 39 | """ 40 | 41 | @doc ~S""" 42 | Paginates data and starts a pseudo-interactive console. 43 | """ 44 | @spec console(any) :: no_return 45 | def console(data) when is_binary(data) do 46 | if IO.ANSI.enabled?() do 47 | start_and_recv(data) 48 | else 49 | prompt = "Pane requires ANSI escape codes to work. Enable? (Yn) " 50 | 51 | case IO.gets(prompt) do 52 | "Y" <> _rest -> enable_ansi_and_start(data) 53 | "y" <> _rest -> enable_ansi_and_start(data) 54 | _ -> :ok 55 | end 56 | end 57 | end 58 | 59 | def console(data), do: data |> inspect(pretty: true) |> console() 60 | 61 | defp enable_ansi_and_start(data) do 62 | Application.put_env(:elixir, :ansi_enabled, true) 63 | start_and_recv(data) 64 | end 65 | 66 | defp start_and_recv(data) do 67 | Pane.Viewer.start_link(data: data) 68 | recv_input() 69 | end 70 | 71 | defp recv_input do 72 | IEx.Helpers.clear() 73 | IO.puts(Pane.Viewer.current_page().data) 74 | 75 | input = <<"\n", Pane.Viewer.prompt()::binary>> |> IO.gets() 76 | process(input) 77 | end 78 | 79 | defp process("f\n") do 80 | Pane.Viewer.first_page() 81 | recv_input() 82 | end 83 | 84 | defp process("l\n") do 85 | Pane.Viewer.last_page() 86 | recv_input() 87 | end 88 | 89 | defp process("j\n") do 90 | Pane.Viewer.next_page() 91 | recv_input() 92 | end 93 | 94 | defp process("k\n") do 95 | Pane.Viewer.prev_page() 96 | recv_input() 97 | end 98 | 99 | defp process("q\n") do 100 | Pane.Viewer.stop() 101 | IEx.Helpers.clear() 102 | :ok 103 | end 104 | 105 | defp process(_else) do 106 | Pane.Viewer.current_page() 107 | recv_input() 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/pane/page.ex: -------------------------------------------------------------------------------- 1 | defmodule Pane.Page do 2 | @moduledoc false 3 | 4 | defstruct data: nil, index: nil 5 | 6 | @max_lines 50 7 | 8 | @type t :: %__MODULE__{ 9 | data: String.t(), 10 | index: pos_integer 11 | } 12 | 13 | @doc ~S""" 14 | Constructs new `Pane.Page` given data and index. 15 | 16 | ## Examples 17 | 18 | iex> Pane.Page.new("test", 1) 19 | %Pane.Page{data: "test", index: 1} 20 | """ 21 | @spec new(String.t(), pos_integer) :: t 22 | def new(data, index) do 23 | %__MODULE__{ 24 | data: data, 25 | index: index 26 | } 27 | end 28 | 29 | @doc ~S""" 30 | Splits data on the newline and chunks it into `Pane.Page` structs. 31 | 32 | ## Examples 33 | 34 | iex> [p1] = Enum.join(1..4, "\n") |> Pane.Page.paginate 35 | iex> p1.index 36 | 0 37 | iex> p1.data 38 | "1\n2\n3\n4" 39 | """ 40 | @spec paginate(String.t(), pos_integer) :: [t] 41 | def paginate(data, max_lines \\ @max_lines) do 42 | data 43 | |> String.split("\n") 44 | |> Enum.chunk_every(max_lines) 45 | |> Enum.map(&Enum.join(&1, "\n")) 46 | |> Enum.with_index() 47 | |> Enum.map(fn {data, index} -> new(data, index) end) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/pane/viewer.ex: -------------------------------------------------------------------------------- 1 | defmodule Pane.Viewer do 2 | @moduledoc false 3 | 4 | defstruct pages: [], total_pages: 0, index: 0 5 | 6 | use GenServer 7 | 8 | @doc ~S""" 9 | Starts a `Pane.Viewer` with given opts. 10 | 11 | 12 | ## Examples 13 | 14 | iex> {:ok, pid} = Pane.Viewer.start_link(data: "test") 15 | iex> is_pid(pid) 16 | true 17 | """ 18 | def start_link(opts \\ []) do 19 | GenServer.start_link(__MODULE__, opts, name: __MODULE__) 20 | end 21 | 22 | def stop, do: GenServer.stop(__MODULE__) 23 | 24 | @doc ~S""" 25 | Returns a `Pane.Viewer` struct with given opts. 26 | 27 | ## Examples 28 | 29 | iex> Pane.Viewer.init(data: "test") 30 | {:ok, %Pane.Viewer{ 31 | index: 0, 32 | total_pages: 1, 33 | pages: [ 34 | %Pane.Page{ 35 | data: "test", 36 | index: 0 37 | } 38 | ] 39 | }} 40 | """ 41 | def init(opts) do 42 | pages = opts[:data] |> Pane.Page.paginate(max_lines() - 2) 43 | 44 | state = %__MODULE__{ 45 | index: 0, 46 | total_pages: Enum.count(pages), 47 | pages: pages 48 | } 49 | 50 | {:ok, state} 51 | end 52 | 53 | def first_page, do: GenServer.call(__MODULE__, :first_page) 54 | 55 | def last_page, do: GenServer.call(__MODULE__, :last_page) 56 | 57 | def next_page, do: GenServer.call(__MODULE__, :next_page) 58 | 59 | def prev_page, do: GenServer.call(__MODULE__, :prev_page) 60 | 61 | def current_page, do: GenServer.call(__MODULE__, :current_page) 62 | 63 | def prompt, do: GenServer.call(__MODULE__, :prompt) 64 | 65 | def handle_call(:first_page, _from, state) do 66 | state = %{state | index: 0} 67 | current = current_page(state) 68 | IO.puts(current.data) 69 | 70 | {:reply, current, state} 71 | end 72 | 73 | def handle_call(:last_page, _from, state) do 74 | state = %{state | index: last_page_index(state)} 75 | current = current_page(state) 76 | IO.puts(current.data) 77 | 78 | {:reply, current, state} 79 | end 80 | 81 | def handle_call(:next_page, _from, state) do 82 | state = inc_page(state) 83 | current = current_page(state) 84 | IO.puts(current.data) 85 | 86 | {:reply, current, state} 87 | end 88 | 89 | def handle_call(:prev_page, _from, state) do 90 | state = dec_page(state) 91 | current = current_page(state) 92 | IO.puts(current.data) 93 | 94 | {:reply, current, state} 95 | end 96 | 97 | def handle_call(:current_page, _from, state) do 98 | {:reply, current_page(state), state} 99 | end 100 | 101 | def handle_call(:prompt, _from, state), do: {:reply, prompt(state), state} 102 | 103 | def current_page(state), do: Enum.at(state.pages, state.index) 104 | 105 | def last_page_index(state), do: Enum.count(state.pages) - 1 106 | 107 | def inc_page(%{index: i, total_pages: total} = state) when i < total - 1 do 108 | %{state | index: state.index + 1} 109 | end 110 | 111 | def inc_page(state), do: state 112 | 113 | def dec_page(%{index: i} = state) when i > 0 do 114 | %{state | index: i - 1} 115 | end 116 | 117 | def dec_page(state), do: state 118 | 119 | def page_description(state) do 120 | "#{state.index + 1} of #{last_page_index(state) + 1}" 121 | end 122 | 123 | def prompt(state) do 124 | "[#{page_description(state)}] (j)next (k)prev (f)first (l)last (q)quit " 125 | end 126 | 127 | def max_lines do 128 | case System.cmd("tput", ["lines"]) do 129 | {count, 0} -> count |> String.trim() |> String.to_integer() 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Pane.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/codedge-llc/pane" 5 | @version "0.5.0" 6 | 7 | def project do 8 | [ 9 | app: :pane, 10 | build_embedded: Mix.env() == :prod, 11 | deps: deps(), 12 | dialyzer: dialyzer(), 13 | docs: docs(), 14 | elixir: "~> 1.13", 15 | name: "Pane", 16 | package: package(), 17 | start_permanent: Mix.env() == :prod, 18 | version: @version 19 | ] 20 | end 21 | 22 | def application do 23 | [extra_applications: [:iex, :logger]] 24 | end 25 | 26 | defp deps do 27 | [ 28 | {:credo, "~> 1.0", only: [:dev], runtime: false}, 29 | {:dialyxir, "~> 1.0", only: [:dev], runtime: false}, 30 | {:ex_doc, ">= 0.0.0", only: [:dev], runtime: false} 31 | ] 32 | end 33 | 34 | defp docs do 35 | [ 36 | extras: [ 37 | "CHANGELOG.md", 38 | "LICENSE.md": [title: "License"] 39 | ], 40 | formatters: ["html"], 41 | main: "Pane", 42 | source_ref: "v#{@version}", 43 | source_url: @source_url 44 | ] 45 | end 46 | 47 | defp dialyzer do 48 | [ 49 | plt_file: {:no_warn, "priv/plts/dialyzer.plt"} 50 | ] 51 | end 52 | 53 | defp package do 54 | [ 55 | description: "Paginated printer for IEx.", 56 | files: ["lib", "mix.exs", "README*", "LICENSE*", "CHANGELOG*"], 57 | licenses: ["MIT"], 58 | links: %{ 59 | "Changelog" => "https://hexdocs.pm/pane/changelog.html", 60 | "GitHub" => "https://github.com/codedge-llc/pane", 61 | "Sponsor" => "https://github.com/sponsors/codedge-llc" 62 | }, 63 | maintainers: ["Henry Popp"] 64 | ] 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "certifi": {:hex, :certifi, "2.5.3", "70bdd7e7188c804f3a30ee0e7c99655bc35d8ac41c23e12325f36ab449b70651", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "ed516acb3929b101208a9d700062d520f3953da3b6b918d866106ffa980e1c10"}, 4 | "credo": {:hex, :credo, "1.7.11", "d3e805f7ddf6c9c854fd36f089649d7cf6ba74c42bc3795d587814e3c9847102", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "56826b4306843253a66e47ae45e98e7d284ee1f95d53d1612bb483f88a8cf219"}, 5 | "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, 6 | "dogma": {:hex, :dogma, "0.1.16", "3c1532e2f63ece4813fe900a16704b8e33264da35fdb0d8a1d05090a3022eef9", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "8533cb896ea527959923f9c3f08e7083e18ff681388ad7c9a599dd5d28e9085f"}, 7 | "earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [:mix], [], "hexpm", "1b34655872366414f69dd987cb121c049f76984b6ac69f52fff6d8fd64d29cfd"}, 8 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 9 | "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, 10 | "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"}, 11 | "excoveralls": {:hex, :excoveralls, "0.18.2", "86efd87a0676a3198ff50b8c77620ea2f445e7d414afa9ec6c4ba84c9f8bdcc2", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "230262c418f0de64077626a498bd4fdf1126d5c2559bb0e6b43deac3005225a4"}, 12 | "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, 13 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, 14 | "hackney": {:hex, :hackney, "1.17.0", "717ea195fd2f898d9fe9f1ce0afcc2621a41ecfe137fae57e7fe6e9484b9aa99", [:rebar3], [{:certifi, "~>2.5", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "64c22225f1ea8855f584720c0e5b3cd14095703af1c9fbc845ba042811dc671c"}, 15 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 16 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 17 | "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"}, 18 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 19 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 20 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 21 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 22 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 23 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 24 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 25 | "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"}, 26 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 27 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 28 | } 29 | -------------------------------------------------------------------------------- /test/pane/page_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Pane.PageTest do 2 | use ExUnit.Case 3 | doctest Pane.Page 4 | end 5 | -------------------------------------------------------------------------------- /test/pane/viewer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Pane.ViewerTest do 2 | use ExUnit.Case 3 | doctest Pane.Viewer, import: true 4 | end 5 | -------------------------------------------------------------------------------- /test/pane_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PaneTest do 2 | use ExUnit.Case 3 | end 4 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------