Skip to content

Commit 7d2137d

Browse files
authored
chore: various wardening (#11)
* chore: various wardening * fix: viewing works on windows now (removed tput)
1 parent d3d1da9 commit 7d2137d

11 files changed

Lines changed: 193 additions & 20 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ jobs:
3131
- name: Set up Elixir
3232
uses: erlef/setup-beam@v1
3333
with:
34-
elixir-version: "1.18.2"
35-
otp-version: "27.2"
34+
elixir-version: "1.19"
35+
otp-version: "28"
3636
- name: Restore dependencies cache
3737
uses: actions/cache@v3
3838
with:
@@ -53,8 +53,8 @@ jobs:
5353
- name: Set up Elixir
5454
uses: erlef/setup-beam@v1
5555
with:
56-
elixir-version: "1.18.0"
57-
otp-version: "27.0.1"
56+
elixir-version: "1.19"
57+
otp-version: "28"
5858
- name: Restore dependencies cache
5959
uses: actions/cache@v3
6060
with:

.tool-versions

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
elixir 1.18.2-otp-27
2-
erlang 27.2
1+
elixir 1.19.4-otp-28
2+
erlang 28.2

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## Unreleased
9+
10+
**Added**
11+
12+
- Better documented typespecs. ([#11](https://github.com/codedge-llc/pane/pull/11))
13+
- More tests for `Pane.Page` and `Pane.Viewer`. ([#11](https://github.com/codedge-llc/pane/pull/11))
14+
15+
**Fixed**
16+
17+
- Terminal size detection on Windows (replaced `tput` with `:io.rows/0`).
18+
([#11](https://github.com/codedge-llc/pane/pull/11)
19+
820
## v0.5.0 - 2024-08-31
921

1022
**Changed**

LICENSE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2017-2025 Codedge LLC (https://www.codedge.io/)
3+
Copyright (c) 2017-2026 Codedge LLC (https://www.codedge.io/)
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,6 @@ Git commit subjects use the [Karma style](http://karma-runner.github.io/5.0/dev/
5555

5656
## License
5757

58-
Copyright (c) 2017-2025 Codedge LLC (https://www.codedge.io/)
58+
Copyright (c) 2017-2026 Codedge LLC (https://www.codedge.io/)
5959

6060
This library is MIT licensed. See the [LICENSE](https://github.com/codedge-llc/pane/blob/main/LICENSE.md) for details.

lib/pane.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ defmodule Pane do
4141
@doc ~S"""
4242
Paginates data and starts a pseudo-interactive console.
4343
"""
44-
@spec console(any) :: no_return
44+
@spec console(any()) :: :ok
4545
def console(data) when is_binary(data) do
4646
if IO.ANSI.enabled?() do
4747
start_and_recv(data)

lib/pane/page.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ defmodule Pane.Page do
77

88
@type t :: %__MODULE__{
99
data: String.t(),
10-
index: pos_integer
10+
index: pos_integer()
1111
}
1212

1313
@doc ~S"""
@@ -18,7 +18,7 @@ defmodule Pane.Page do
1818
iex> Pane.Page.new("test", 1)
1919
%Pane.Page{data: "test", index: 1}
2020
"""
21-
@spec new(String.t(), pos_integer) :: t
21+
@spec new(String.t(), pos_integer()) :: t()
2222
def new(data, index) do
2323
%__MODULE__{
2424
data: data,
@@ -37,7 +37,7 @@ defmodule Pane.Page do
3737
iex> p1.data
3838
"1\n2\n3\n4"
3939
"""
40-
@spec paginate(String.t(), pos_integer) :: [t]
40+
@spec paginate(String.t(), pos_integer()) :: [t()]
4141
def paginate(data, max_lines \\ @max_lines) do
4242
data
4343
|> String.split("\n")

lib/pane/viewer.ex

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ defmodule Pane.Viewer do
55

66
use GenServer
77

8+
@type t :: %__MODULE__{
9+
pages: [Pane.Page.t()],
10+
total_pages: non_neg_integer(),
11+
index: non_neg_integer()
12+
}
13+
14+
@default_max_lines 50
15+
816
@doc ~S"""
917
Starts a `Pane.Viewer` with given opts.
1018
@@ -15,10 +23,12 @@ defmodule Pane.Viewer do
1523
iex> is_pid(pid)
1624
true
1725
"""
26+
@spec start_link(keyword()) :: GenServer.on_start()
1827
def start_link(opts \\ []) do
1928
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
2029
end
2130

31+
@spec stop :: :ok
2232
def stop, do: GenServer.stop(__MODULE__)
2333

2434
@doc ~S"""
@@ -50,16 +60,22 @@ defmodule Pane.Viewer do
5060
{:ok, state}
5161
end
5262

63+
@spec first_page :: Pane.Page.t()
5364
def first_page, do: GenServer.call(__MODULE__, :first_page)
5465

66+
@spec last_page :: Pane.Page.t()
5567
def last_page, do: GenServer.call(__MODULE__, :last_page)
5668

69+
@spec next_page :: Pane.Page.t()
5770
def next_page, do: GenServer.call(__MODULE__, :next_page)
5871

72+
@spec prev_page :: Pane.Page.t()
5973
def prev_page, do: GenServer.call(__MODULE__, :prev_page)
6074

75+
@spec current_page :: Pane.Page.t()
6176
def current_page, do: GenServer.call(__MODULE__, :current_page)
6277

78+
@spec prompt :: String.t()
6379
def prompt, do: GenServer.call(__MODULE__, :prompt)
6480

6581
def handle_call(:first_page, _from, state) do
@@ -100,33 +116,41 @@ defmodule Pane.Viewer do
100116

101117
def handle_call(:prompt, _from, state), do: {:reply, prompt(state), state}
102118

119+
@spec current_page(t()) :: Pane.Page.t()
103120
def current_page(state), do: Enum.at(state.pages, state.index)
104121

122+
@spec last_page_index(t()) :: non_neg_integer()
105123
def last_page_index(state), do: Enum.count(state.pages) - 1
106124

125+
@spec inc_page(t()) :: t()
107126
def inc_page(%{index: i, total_pages: total} = state) when i < total - 1 do
108127
%{state | index: state.index + 1}
109128
end
110129

111130
def inc_page(state), do: state
112131

132+
@spec dec_page(t()) :: t()
113133
def dec_page(%{index: i} = state) when i > 0 do
114134
%{state | index: i - 1}
115135
end
116136

117137
def dec_page(state), do: state
118138

139+
@spec page_description(t()) :: String.t()
119140
def page_description(state) do
120141
"#{state.index + 1} of #{last_page_index(state) + 1}"
121142
end
122143

144+
@spec prompt(t()) :: String.t()
123145
def prompt(state) do
124146
"[#{page_description(state)}] (j)next (k)prev (f)first (l)last (q)quit "
125147
end
126148

149+
@spec max_lines :: pos_integer()
127150
def max_lines do
128-
case System.cmd("tput", ["lines"]) do
129-
{count, 0} -> count |> String.trim() |> String.to_integer()
151+
case :io.rows() do
152+
{:ok, rows} -> rows
153+
{:error, _} -> @default_max_lines
130154
end
131155
end
132156
end

mix.lock

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
%{
22
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
33
"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"},
4+
"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"},
5+
"dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"},
66
"dogma": {:hex, :dogma, "0.1.16", "3c1532e2f63ece4813fe900a16704b8e33264da35fdb0d8a1d05090a3022eef9", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "8533cb896ea527959923f9c3f08e7083e18ff681388ad7c9a599dd5d28e9085f"},
77
"earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [:mix], [], "hexpm", "1b34655872366414f69dd987cb121c049f76984b6ac69f52fff6d8fd64d29cfd"},
88
"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"},
9+
"erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"},
10+
"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"},
1111
"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"},
1212
"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"},
13+
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
1414
"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"},
1515
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
1616
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
1717
"jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"},
1818
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
1919
"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"},
20+
"makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"},
2121
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
2222
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
2323
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},

test/pane/page_test.exs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,50 @@
11
defmodule Pane.PageTest do
22
use ExUnit.Case
33
doctest Pane.Page
4+
5+
alias Pane.Page
6+
7+
describe "paginate/2" do
8+
test "empty string returns single page" do
9+
[page] = Page.paginate("")
10+
assert page.data == ""
11+
assert page.index == 0
12+
end
13+
14+
test "data within max_lines returns single page" do
15+
data = Enum.join(1..5, "\n")
16+
[page] = Page.paginate(data, 10)
17+
assert page.data == data
18+
assert page.index == 0
19+
end
20+
21+
test "data exceeding max_lines returns multiple pages" do
22+
data = Enum.join(1..10, "\n")
23+
pages = Page.paginate(data, 3)
24+
assert length(pages) == 4
25+
assert Enum.map(pages, & &1.index) == [0, 1, 2, 3]
26+
end
27+
28+
test "data at exact max_lines boundary returns single page" do
29+
data = Enum.join(1..5, "\n")
30+
[page] = Page.paginate(data, 5)
31+
assert page.data == data
32+
assert page.index == 0
33+
end
34+
35+
test "data one over max_lines boundary returns two pages" do
36+
data = Enum.join(1..6, "\n")
37+
pages = Page.paginate(data, 5)
38+
assert length(pages) == 2
39+
assert List.first(pages).data == Enum.join(1..5, "\n")
40+
assert List.last(pages).data == "6"
41+
end
42+
43+
test "pages contain correct data splits" do
44+
data = Enum.join(1..6, "\n")
45+
[p1, p2] = Page.paginate(data, 3)
46+
assert p1.data == "1\n2\n3"
47+
assert p2.data == "4\n5\n6"
48+
end
49+
end
450
end

0 commit comments

Comments
 (0)