1import gleam/list
2import gleam/result
3import gleam/string
4import term_size
5
6pub fn term_width() -> Int {
7 term_size.columns()
8 |> result.unwrap(80)
9}
10
11pub fn shorten_url(url: String, to max_length: Int) -> String {
12 let chunks = case url {
13 "https://" <> rest -> ["https:/", ..string.split(rest, on: "/")]
14 _ -> string.split(url, on: "/")
15 }
16
17 // We want to reduce the max length further to offset the `/` we'll
18 // be adding back to join the remaining pieces.
19 let max_length = max_length - list.length(chunks)
20 case shorten(chunks, to: max_length) {
21 Error(_) -> url
22 Ok(#(left, right)) ->
23 string.join(left, "/") <> "/.../" <> string.join(right, "/")
24 }
25}
26
27/// Shortens a list of strings by removing pieces from the middle until it gets
28/// down to the desired length, returning its two remaining halves.
29/// Returns an error if it couldn't shorten the list.
30///
31fn shorten(
32 strings: List(String),
33 to max_length: Int,
34) -> Result(#(List(String), List(String)), Nil) {
35 let initial_length =
36 list.fold(over: strings, from: 0, with: fn(acc, string) {
37 acc + string.length(string)
38 })
39
40 // We want to divide the strings in two halves and remove items starting from
41 // the middle going in both directions until we get to the desired length or
42 // we can shorten it any further.
43 let middle = list.length(strings) / 2
44 let #(left, right) = list.split(strings, middle)
45 // It's important we reverse the left part because we want to remove pieces
46 // from its end, that is the part nearer to the middle of `strings`.
47 let left = list.reverse(left)
48 case do_shorten(left, right, False, Right, initial_length, max_length) {
49 // Remember that the left part was reversed so that `do_shorten` could
50 // remove items from its start! We have to reverse it back to normal.
51 Ok(#(new_left, new_right)) -> Ok(#(list.reverse(new_left), new_right))
52 Error(Nil) -> Error(Nil)
53 }
54}
55
56type End {
57 Left
58 Right
59}
60
61/// Drops strings from the start of each list in turns until it shrinks it down
62/// to the desired size.
63/// If it could acutally shorten the lists it returns `Ok` wrapping them,
64/// otherwise it returns `Error(Nil)`.
65///
66fn do_shorten(
67 left: List(String),
68 right: List(String),
69 shortened: Bool,
70 from: End,
71 current_length: Int,
72 max_length: Int,
73) -> Result(#(List(String), List(String)), Nil) {
74 case current_length <= max_length, left, right, from {
75 // If we're already shorter than the maximum allowed length we don't shrink
76 // the lists any further.
77 True, _, _, _ ->
78 case shortened {
79 True -> Ok(#(left, right))
80 False -> Error(Nil)
81 }
82
83 // If we're down to one or less chunks we can't shorten it any further.
84 _, [], [_], _ | _, [], [], _ ->
85 case shortened {
86 True -> Ok(#(left, right))
87 False -> Error(Nil)
88 }
89
90 // We always want to keep the rightmost chunk. So we make sure to
91 // never drop it if it's the last one remaining on the right.
92 _, left, [_] as right, Right ->
93 do_shorten(left, right, shortened, Left, current_length, max_length)
94
95 // Otherwise we remove a piece from the end specified by `end` until we
96 // reach the desired size.
97 _, [dropped, ..left], right, Left -> {
98 let new_length = current_length - string.length(dropped)
99 do_shorten(left, right, True, Right, new_length, max_length)
100 }
101 _, left, [dropped, ..right], Right -> {
102 let new_length = current_length - string.length(dropped)
103 do_shorten(left, right, True, Left, new_length, max_length)
104 }
105
106 // If we can't remove from the desired side we try shrinking further from
107 // the other end.
108 _, [], right, Left ->
109 do_shorten([], right, shortened, Right, current_length, max_length)
110 _, left, [], Right ->
111 do_shorten(left, [], shortened, Left, current_length, max_length)
112 }
113}