From 099556096d5b461fe90aa37f55a5df5e8604733b Mon Sep 17 00:00:00 2001 From: Henry Popp Date: Sun, 22 Feb 2026 12:13:14 -0600 Subject: [PATCH 1/2] chore: various wardening --- .github/workflows/ci.yml | 8 ++-- .tool-versions | 4 +- CHANGELOG.md | 6 +++ LICENSE.md | 2 +- README.md | 2 +- lib/pane.ex | 2 +- lib/pane/viewer.ex | 14 ++++++ mix.lock | 12 +++--- test/pane/page_test.exs | 46 ++++++++++++++++++++ test/pane/viewer_test.exs | 91 +++++++++++++++++++++++++++++++++++++++ 10 files changed, 172 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eaa1165..5705aeb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,8 +31,8 @@ jobs: - name: Set up Elixir uses: erlef/setup-beam@v1 with: - elixir-version: "1.18.2" - otp-version: "27.2" + elixir-version: "1.19" + otp-version: "28" - name: Restore dependencies cache uses: actions/cache@v3 with: @@ -53,8 +53,8 @@ jobs: - name: Set up Elixir uses: erlef/setup-beam@v1 with: - elixir-version: "1.18.0" - otp-version: "27.0.1" + elixir-version: "1.19" + otp-version: "28" - name: Restore dependencies cache uses: actions/cache@v3 with: diff --git a/.tool-versions b/.tool-versions index 7abdf07..85434ba 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -elixir 1.18.2-otp-27 -erlang 27.2 +elixir 1.19.4-otp-28 +erlang 28.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2811f26..7a4df38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +**Added** + +- Better documented typespecs. + ## v0.5.0 - 2024-08-31 **Changed** diff --git a/LICENSE.md b/LICENSE.md index 5f6c547..e31562e 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017-2025 Codedge LLC (https://www.codedge.io/) +Copyright (c) 2017-2026 Codedge LLC (https://www.codedge.io/) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 1c71485..66daeae 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,6 @@ Git commit subjects use the [Karma style](http://karma-runner.github.io/5.0/dev/ ## License -Copyright (c) 2017-2025 Codedge LLC (https://www.codedge.io/) +Copyright (c) 2017-2026 Codedge LLC (https://www.codedge.io/) This library is MIT licensed. See the [LICENSE](https://github.com/codedge-llc/pane/blob/main/LICENSE.md) for details. diff --git a/lib/pane.ex b/lib/pane.ex index b4baae9..a9c58ca 100644 --- a/lib/pane.ex +++ b/lib/pane.ex @@ -41,7 +41,7 @@ defmodule Pane do @doc ~S""" Paginates data and starts a pseudo-interactive console. """ - @spec console(any) :: no_return + @spec console(any) :: :ok def console(data) when is_binary(data) do if IO.ANSI.enabled?() do start_and_recv(data) diff --git a/lib/pane/viewer.ex b/lib/pane/viewer.ex index 3cbff79..0f4f0fe 100644 --- a/lib/pane/viewer.ex +++ b/lib/pane/viewer.ex @@ -5,6 +5,12 @@ defmodule Pane.Viewer do use GenServer + @type t :: %__MODULE__{ + pages: [Pane.Page.t()], + total_pages: non_neg_integer, + index: non_neg_integer + } + @doc ~S""" Starts a `Pane.Viewer` with given opts. @@ -15,10 +21,12 @@ defmodule Pane.Viewer do iex> is_pid(pid) true """ + @spec start_link(keyword) :: GenServer.on_start() def start_link(opts \\ []) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end + @spec stop :: :ok def stop, do: GenServer.stop(__MODULE__) @doc ~S""" @@ -50,16 +58,22 @@ defmodule Pane.Viewer do {:ok, state} end + @spec first_page :: Pane.Page.t() def first_page, do: GenServer.call(__MODULE__, :first_page) + @spec last_page :: Pane.Page.t() def last_page, do: GenServer.call(__MODULE__, :last_page) + @spec next_page :: Pane.Page.t() def next_page, do: GenServer.call(__MODULE__, :next_page) + @spec prev_page :: Pane.Page.t() def prev_page, do: GenServer.call(__MODULE__, :prev_page) + @spec current_page :: Pane.Page.t() def current_page, do: GenServer.call(__MODULE__, :current_page) + @spec prompt :: String.t() def prompt, do: GenServer.call(__MODULE__, :prompt) def handle_call(:first_page, _from, state) do diff --git a/mix.lock b/mix.lock index 37ac3ab..1679772 100644 --- a/mix.lock +++ b/mix.lock @@ -1,23 +1,23 @@ %{ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "certifi": {:hex, :certifi, "2.5.3", "70bdd7e7188c804f3a30ee0e7c99655bc35d8ac41c23e12325f36ab449b70651", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "ed516acb3929b101208a9d700062d520f3953da3b6b918d866106ffa980e1c10"}, - "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"}, - "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, + "credo": {:hex, :credo, "1.7.16", "a9f1389d13d19c631cb123c77a813dbf16449a2aebf602f590defa08953309d4", [: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", "d0562af33756b21f248f066a9119e3890722031b6d199f22e3cf95550e4f1579"}, + "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, "dogma": {:hex, :dogma, "0.1.16", "3c1532e2f63ece4813fe900a16704b8e33264da35fdb0d8a1d05090a3022eef9", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "8533cb896ea527959923f9c3f08e7083e18ff681388ad7c9a599dd5d28e9085f"}, "earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [:mix], [], "hexpm", "1b34655872366414f69dd987cb121c049f76984b6ac69f52fff6d8fd64d29cfd"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, - "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, - "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"}, + "erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"}, + "ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [:mix], [{:earmark_parser, "~> 1.4.44", [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", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"}, "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"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, - "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, + "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "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"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "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"}, - "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, diff --git a/test/pane/page_test.exs b/test/pane/page_test.exs index d93bb78..dd8be4a 100644 --- a/test/pane/page_test.exs +++ b/test/pane/page_test.exs @@ -1,4 +1,50 @@ defmodule Pane.PageTest do use ExUnit.Case doctest Pane.Page + + alias Pane.Page + + describe "paginate/2" do + test "empty string returns single page" do + [page] = Page.paginate("") + assert page.data == "" + assert page.index == 0 + end + + test "data within max_lines returns single page" do + data = Enum.join(1..5, "\n") + [page] = Page.paginate(data, 10) + assert page.data == data + assert page.index == 0 + end + + test "data exceeding max_lines returns multiple pages" do + data = Enum.join(1..10, "\n") + pages = Page.paginate(data, 3) + assert length(pages) == 4 + assert Enum.map(pages, & &1.index) == [0, 1, 2, 3] + end + + test "data at exact max_lines boundary returns single page" do + data = Enum.join(1..5, "\n") + [page] = Page.paginate(data, 5) + assert page.data == data + assert page.index == 0 + end + + test "data one over max_lines boundary returns two pages" do + data = Enum.join(1..6, "\n") + pages = Page.paginate(data, 5) + assert length(pages) == 2 + assert List.first(pages).data == Enum.join(1..5, "\n") + assert List.last(pages).data == "6" + end + + test "pages contain correct data splits" do + data = Enum.join(1..6, "\n") + [p1, p2] = Page.paginate(data, 3) + assert p1.data == "1\n2\n3" + assert p2.data == "4\n5\n6" + end + end end diff --git a/test/pane/viewer_test.exs b/test/pane/viewer_test.exs index ea74367..26304b8 100644 --- a/test/pane/viewer_test.exs +++ b/test/pane/viewer_test.exs @@ -1,4 +1,95 @@ defmodule Pane.ViewerTest do use ExUnit.Case doctest Pane.Viewer, import: true + + alias Pane.{Page, Viewer} + + defp build_state(num_pages, index \\ 0) do + pages = Enum.map(0..(num_pages - 1), &Page.new("page #{&1}", &1)) + + %Viewer{ + pages: pages, + total_pages: num_pages, + index: index + } + end + + describe "current_page/1" do + test "returns page at current index" do + state = build_state(3, 1) + page = Viewer.current_page(state) + assert page.data == "page 1" + assert page.index == 1 + end + + test "returns first page when index is 0" do + state = build_state(3) + page = Viewer.current_page(state) + assert page.data == "page 0" + end + end + + describe "inc_page/1" do + test "increments index" do + state = build_state(3, 0) + assert Viewer.inc_page(state).index == 1 + end + + test "does not increment past last page" do + state = build_state(3, 2) + assert Viewer.inc_page(state).index == 2 + end + + test "does not increment on single page" do + state = build_state(1, 0) + assert Viewer.inc_page(state).index == 0 + end + end + + describe "dec_page/1" do + test "decrements index" do + state = build_state(3, 2) + assert Viewer.dec_page(state).index == 1 + end + + test "does not decrement below 0" do + state = build_state(3, 0) + assert Viewer.dec_page(state).index == 0 + end + end + + describe "last_page_index/1" do + test "returns last index" do + assert Viewer.last_page_index(build_state(5)) == 4 + end + + test "returns 0 for single page" do + assert Viewer.last_page_index(build_state(1)) == 0 + end + end + + describe "page_description/1" do + test "formats current position" do + state = build_state(5, 2) + assert Viewer.page_description(state) == "3 of 5" + end + + test "formats first page" do + state = build_state(3, 0) + assert Viewer.page_description(state) == "1 of 3" + end + + test "formats last page" do + state = build_state(3, 2) + assert Viewer.page_description(state) == "3 of 3" + end + end + + describe "prompt/1" do + test "includes page description and commands" do + state = build_state(3, 1) + prompt = Viewer.prompt(state) + assert prompt == "[2 of 3] (j)next (k)prev (f)first (l)last (q)quit " + end + end end From 59f092543e50a8a016e40d44fc8c18ac8ec87251 Mon Sep 17 00:00:00 2001 From: Henry Popp Date: Sun, 22 Feb 2026 12:21:30 -0600 Subject: [PATCH 2/2] fix: viewing works on windows now (removed tput) --- CHANGELOG.md | 8 +++++++- lib/pane.ex | 2 +- lib/pane/page.ex | 6 +++--- lib/pane/viewer.ex | 20 +++++++++++++++----- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a4df38..9bff89b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 **Added** -- Better documented typespecs. +- Better documented typespecs. ([#11](https://github.com/codedge-llc/pane/pull/11)) +- More tests for `Pane.Page` and `Pane.Viewer`. ([#11](https://github.com/codedge-llc/pane/pull/11)) + +**Fixed** + +- Terminal size detection on Windows (replaced `tput` with `:io.rows/0`). + ([#11](https://github.com/codedge-llc/pane/pull/11) ## v0.5.0 - 2024-08-31 diff --git a/lib/pane.ex b/lib/pane.ex index a9c58ca..33ccd76 100644 --- a/lib/pane.ex +++ b/lib/pane.ex @@ -41,7 +41,7 @@ defmodule Pane do @doc ~S""" Paginates data and starts a pseudo-interactive console. """ - @spec console(any) :: :ok + @spec console(any()) :: :ok def console(data) when is_binary(data) do if IO.ANSI.enabled?() do start_and_recv(data) diff --git a/lib/pane/page.ex b/lib/pane/page.ex index e6bb93c..7d2aea3 100644 --- a/lib/pane/page.ex +++ b/lib/pane/page.ex @@ -7,7 +7,7 @@ defmodule Pane.Page do @type t :: %__MODULE__{ data: String.t(), - index: pos_integer + index: pos_integer() } @doc ~S""" @@ -18,7 +18,7 @@ defmodule Pane.Page do iex> Pane.Page.new("test", 1) %Pane.Page{data: "test", index: 1} """ - @spec new(String.t(), pos_integer) :: t + @spec new(String.t(), pos_integer()) :: t() def new(data, index) do %__MODULE__{ data: data, @@ -37,7 +37,7 @@ defmodule Pane.Page do iex> p1.data "1\n2\n3\n4" """ - @spec paginate(String.t(), pos_integer) :: [t] + @spec paginate(String.t(), pos_integer()) :: [t()] def paginate(data, max_lines \\ @max_lines) do data |> String.split("\n") diff --git a/lib/pane/viewer.ex b/lib/pane/viewer.ex index 0f4f0fe..973c40e 100644 --- a/lib/pane/viewer.ex +++ b/lib/pane/viewer.ex @@ -7,10 +7,12 @@ defmodule Pane.Viewer do @type t :: %__MODULE__{ pages: [Pane.Page.t()], - total_pages: non_neg_integer, - index: non_neg_integer + total_pages: non_neg_integer(), + index: non_neg_integer() } + @default_max_lines 50 + @doc ~S""" Starts a `Pane.Viewer` with given opts. @@ -21,7 +23,7 @@ defmodule Pane.Viewer do iex> is_pid(pid) true """ - @spec start_link(keyword) :: GenServer.on_start() + @spec start_link(keyword()) :: GenServer.on_start() def start_link(opts \\ []) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end @@ -114,33 +116,41 @@ defmodule Pane.Viewer do def handle_call(:prompt, _from, state), do: {:reply, prompt(state), state} + @spec current_page(t()) :: Pane.Page.t() def current_page(state), do: Enum.at(state.pages, state.index) + @spec last_page_index(t()) :: non_neg_integer() def last_page_index(state), do: Enum.count(state.pages) - 1 + @spec inc_page(t()) :: t() def inc_page(%{index: i, total_pages: total} = state) when i < total - 1 do %{state | index: state.index + 1} end def inc_page(state), do: state + @spec dec_page(t()) :: t() def dec_page(%{index: i} = state) when i > 0 do %{state | index: i - 1} end def dec_page(state), do: state + @spec page_description(t()) :: String.t() def page_description(state) do "#{state.index + 1} of #{last_page_index(state) + 1}" end + @spec prompt(t()) :: String.t() def prompt(state) do "[#{page_description(state)}] (j)next (k)prev (f)first (l)last (q)quit " end + @spec max_lines :: pos_integer() def max_lines do - case System.cmd("tput", ["lines"]) do - {count, 0} -> count |> String.trim() |> String.to_integer() + case :io.rows() do + {:ok, rows} -> rows + {:error, _} -> @default_max_lines end end end