Skip to content

Commit 35265f6

Browse files
authored
Merge pull request #6 from massix/feat/configuration
Add configuration
2 parents 7d954ea + b5720a1 commit 35265f6

File tree

14 files changed

+422
-31
lines changed

14 files changed

+422
-31
lines changed

Makefile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ GLEAM := $(shell command -v gleam)
22
DOCKER_HUB_REPOSITORY ?= massix86/gleeter
33
DOCKER_PLATFORM ?= linux/amd64
44

5-
.PHONY: test
6-
75
all: build
86

97
.PHONY: deps
@@ -26,6 +24,10 @@ check-format:
2624
package: build
2725
$(GLEAM) export erlang-shipment
2826

27+
.PHONY: serve
28+
serve: build
29+
GLEETER_DEBUG=1 $(GLEAM) run -- serve 8080 comics
30+
2931
.PHONY: docker
3032
docker:
3133
nix build .#version-file

README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,54 @@ Gleeter understands the following commands:
6060
* `id <number>`: Fetches the comic with the specified ID and displays it in the terminal. Replace `<number>` with the desired comic ID.
6161
* `serve <port> <base_path>`: Starts a web server to serve the comics. `<port>` is optional and defaults to 8080. `<base_path>` is also optional and defaults to "". For example, `gleeter serve 3000 /comics` will start the server on port 3000 and serve the comics under the `/comics` path.
6262

63+
## Configuration File
64+
65+
Gleeter uses a TOML configuration file to customize its behavior. The configuration file allows you to configure the starting comic for random mode, set screen dimensions, and define aliases for comic IDs.
66+
67+
### Syntax
68+
69+
Here's an example configuration file (based on `test_data/config.toml`):
70+
71+
```toml
72+
random_start = 38
73+
74+
[screen]
75+
max_cols = 80
76+
max_lines = 23
77+
78+
[[alias]]
79+
name = "bobbytables"
80+
type = "id"
81+
id = 987
82+
83+
[[alias]]
84+
name = "rnd"
85+
type = "random"
86+
87+
[[alias]]
88+
name = "l"
89+
type = "latest"
90+
```
91+
92+
### Structure and Usage
93+
94+
The configuration file is loaded when Gleeter starts. Gleeter looks for the configuration file in `$XDG_CONFIG_HOME/gleeter/config.toml` or `$HOME/.config/gleeter/config.toml`. If neither of these is found, it uses `./config.toml`. The settings defined in the file are used to initialize the application and control its behavior.
95+
96+
* **`random_start`**: Specifies the starting comic number for the random comic selection. If not specified, a default value is used.
97+
98+
* **`[screen]`**: This section configures the screen dimensions for displaying comics.
99+
* `max_cols`: Specifies the maximum number of columns to use for displaying the comic.
100+
* `max_lines`: Specifies the maximum number of lines to use for displaying the comic.
101+
102+
* **`[[alias]]`**: This section defines aliases for accessing comics. You can define multiple aliases.
103+
* `name`: The name of the alias.
104+
* `type`: The type of alias. Valid values are `"id"`, `"random"`, and `"latest"`.
105+
* `id`: (Only required for `"id"` aliases) The comic ID to associate with the alias.
106+
107+
The `src/gleeter/config.gleam` file defines the structure of the configuration data and how it is parsed from the TOML file.
108+
109+
To modify Gleeter's behavior, simply edit the configuration file and restart the application.
110+
63111
### Serve mode details
64112

65113
When Gleeter is run in `serve` mode, it starts an HTTP server that exposes the following endpoints:
@@ -78,6 +126,26 @@ You can customize the base path for these endpoints by using the `<base_path>` a
78126

79127
Gleeter also supports receiving the terminal size via HTTP headers. You can send the `X-TERMINAL-COLUMNS` and `X-TERMINAL-ROWS` headers with your request to specify the terminal size. This allows Gleeter to properly format the comic for your terminal. If these headers are not provided, Gleeter will use a default terminal size. Please be aware that for the resizing to work, you need to send **both** headers.
80128

129+
On top of that, Serve mode will also honour all the aliases defined in the [configuration file](#configuration-file), and expose them all at the root of the `base_path`. As a quick example, imagine you have the following defined in the `config.toml` file:
130+
```toml
131+
[[alias]]
132+
name = "bobbytables"
133+
type = "id"
134+
id = 987
135+
136+
[[alias]]
137+
name = "rnd"
138+
type = "random"
139+
140+
[[alias]]
141+
name = "l"
142+
type = "latest"
143+
```
144+
145+
And you start the server with `gleeter serve 8080 /comics`, on top of the URLs mentioned above, you will also get the following:
146+
* `/comics/bobbytables`, which will be an alias for `/comics/id/987`
147+
* `/comics/rnd`, which will be an alias for `/comics/random`
148+
* `/comics/l`, which will be an alias for `/comics/latest`
81149

82150
## How to install
83151

flake.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
pkgs = import nixpkgs { inherit system; };
1111
inherit (pkgs) mkShell;
1212
gleamPackagesHash = "sha256-DIY9OA3ZigaVC2gxvwgCrF8rjNIraSy7mVqudp62x4M=";
13-
version = "1.2.0";
13+
version = "1.3.0";
1414
pname = "gleeter";
1515
src = ./.;
1616
gleam-helper = pkgs.callPackage ./nix/gleam-helper.nix { };

gleam.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ envoy = ">= 1.0.2 and < 2.0.0"
1818
term_size = ">= 1.0.1 and < 2.0.0"
1919
messua = ">= 1.1.1 and < 2.0.0"
2020
gleam_erlang = ">= 0.34.0 and < 1.0.0"
21+
tom = ">= 1.1.1 and < 2.0.0"
2122

2223
[dev-dependencies]
2324
startest = ">= 0.6.0 and < 1.0.0"

manifest.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,4 @@ simplifile = { version = ">= 2.2.1 and < 3.0.0" }
7575
sqlight = { version = ">= 1.0.1 and < 2.0.0" }
7676
startest = { version = ">= 0.6.0 and < 1.0.0" }
7777
term_size = { version = ">= 1.0.1 and < 2.0.0" }
78+
tom = { version = ">= 1.1.1 and < 2.0.0" }

src/application_behavior.gleam

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import argv
22
import gleam/int
3+
import gleam/list
34
import gleam/result
5+
import gleeter/config
46

57
pub type ApplicationBehavior {
68
RandomComic
@@ -11,10 +13,12 @@ pub type ApplicationBehavior {
1113
Serve(port: Int, base_path: String)
1214
}
1315

14-
pub fn get_application_behavior() -> ApplicationBehavior {
16+
pub fn get_application_behavior(
17+
cfg: config.Configuration,
18+
) -> ApplicationBehavior {
1519
let args = argv.load().arguments
1620

17-
parse_arguments(args)
21+
parse_arguments(args, cfg.aliases)
1822
}
1923

2024
fn parse_serve(args: List(String)) -> ApplicationBehavior {
@@ -31,19 +35,33 @@ fn parse_serve(args: List(String)) -> ApplicationBehavior {
3135
}
3236
}
3337

34-
pub fn parse_arguments(args: List(String)) -> ApplicationBehavior {
38+
pub fn parse_arguments(
39+
args: List(String),
40+
aliases: List(config.Alias),
41+
) -> ApplicationBehavior {
3542
case args {
36-
[] | ["help", ..] -> Help
37-
["version", ..] -> PrintVersion
43+
[] | ["help", ..] | ["--help", ..] -> Help
44+
["version", ..] | ["--version", ..] -> PrintVersion
3845
["random", ..] -> RandomComic
3946
["latest", ..] -> LatestComic
4047
["id", id, ..] -> {
4148
case int.parse(id) {
4249
Ok(id) -> WithIDComic(id)
43-
_ -> LatestComic
50+
_ -> Help
4451
}
4552
}
4653
["serve", ..rest] -> parse_serve(rest)
47-
_ -> PrintVersion
54+
[alias] -> {
55+
case list.filter(aliases, fn(x) { x.name == alias }) |> list.first() {
56+
Ok(alias) ->
57+
case alias {
58+
config.IdAlias(_, id) -> WithIDComic(id)
59+
config.LatestAlias(_) -> LatestComic
60+
config.RandomAlias(_) -> RandomComic
61+
}
62+
Error(_) -> Help
63+
}
64+
}
65+
_ -> Help
4866
}
4967
}

src/gleeter.gleam

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import gleam/io
88
import gleam/list
99
import gleam/option
1010
import gleam/result
11+
import gleeter/config
1112
import kitty/graphics
1213
import png
1314
import serve
@@ -70,14 +71,21 @@ fn get_comic(cache: cache.Cache, id: Int) -> Result(cache.ComicWithData, Nil) {
7071
}
7172
}
7273

73-
fn print_comic(cache: cache.Cache, in: PrintComic) -> Result(Nil, Nil) {
74+
fn print_comic(
75+
cache: cache.Cache,
76+
config: config.Configuration,
77+
in: PrintComic,
78+
) -> Result(Nil, Nil) {
7479
use cached_comic <- result.try(case in {
7580
Latest -> get_comic(cache, 0)
7681
Random -> {
77-
use api.Xkcd(number:, ..) <- result.try(
82+
use api.Xkcd(number: highest, ..) <- result.try(
7883
api.get_latest() |> result.map_error(print_api_error),
7984
)
80-
let random_comic = int.random(number)
85+
let random_comic = case config.random_start {
86+
option.Some(start) -> int.random(highest - start) + start
87+
option.None -> int.random(highest)
88+
}
8189
get_comic(cache, random_comic)
8290
}
8391
ID(id) -> get_comic(cache, id)
@@ -87,6 +95,26 @@ fn print_comic(cache: cache.Cache, in: PrintComic) -> Result(Nil, Nil) {
8795
use image_size <- result.try(png.get_image_size(raw_data))
8896
let terminal_size = graphics.get_terminal_size()
8997

98+
// Set new max_cols and max_lines if they were specified in the configuration
99+
let terminal_size = case config.max_lines, config.max_cols {
100+
option.Some(max_rows), option.Some(max_cols) ->
101+
graphics.TerminalSize(
102+
int.min(max_rows, terminal_size.rows),
103+
int.min(max_cols, terminal_size.columns),
104+
)
105+
option.Some(max_rows), option.None ->
106+
graphics.TerminalSize(
107+
int.min(max_rows, terminal_size.rows),
108+
terminal_size.columns,
109+
)
110+
option.None, option.Some(max_cols) ->
111+
graphics.TerminalSize(
112+
terminal_size.rows,
113+
int.min(max_cols, terminal_size.columns),
114+
)
115+
option.None, option.None -> terminal_size
116+
}
117+
90118
let api.Xkcd(publication_date:, title:, alternative_text:, number:, link:, ..) =
91119
xkcd
92120
io.print("[" <> int.to_string(number) <> "] ")
@@ -120,12 +148,21 @@ fn print_help() -> Result(Nil, Nil) {
120148
pub fn main() -> Result(Nil, Nil) {
121149
let cache = cache.new(cache.get_cache_location())
122150
let now = birl.now()
123-
let r = case application_behavior.get_application_behavior() {
151+
let configuration_path = config.get_configuration_file()
152+
debug_print("Configuration path: " <> configuration_path)
153+
154+
let configuration = config.parse(configuration_path)
155+
156+
let r = case application_behavior.get_application_behavior(configuration) {
124157
application_behavior.PrintVersion -> print_version()
125-
application_behavior.LatestComic -> print_comic(cache, Latest)
126-
application_behavior.RandomComic -> print_comic(cache, Random)
127-
application_behavior.WithIDComic(id) -> print_comic(cache, ID(id))
128-
application_behavior.Serve(p, b) -> serve.serve(p, b, cache) |> Ok
158+
application_behavior.LatestComic ->
159+
print_comic(cache, configuration, Latest)
160+
application_behavior.RandomComic ->
161+
print_comic(cache, configuration, Random)
162+
application_behavior.WithIDComic(id) ->
163+
print_comic(cache, configuration, ID(id))
164+
application_behavior.Serve(p, b) ->
165+
serve.serve(p, b, cache, configuration) |> Ok
129166
application_behavior.Help -> print_help()
130167
}
131168
let end = birl.now()

src/gleeter/config.gleam

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import envoy
2+
import gleam/dict
3+
import gleam/list
4+
import gleam/option
5+
import gleam/result
6+
import simplifile
7+
import tom
8+
9+
pub type Configuration {
10+
Configuration(
11+
random_start: option.Option(Int),
12+
max_cols: option.Option(Int),
13+
max_lines: option.Option(Int),
14+
aliases: List(Alias),
15+
)
16+
}
17+
18+
pub type Alias {
19+
IdAlias(name: String, comic_id: Int)
20+
LatestAlias(name: String)
21+
RandomAlias(name: String)
22+
}
23+
24+
pub type ConfigFile =
25+
String
26+
27+
fn default() -> Configuration {
28+
Configuration(
29+
random_start: option.None,
30+
max_cols: option.None,
31+
max_lines: option.None,
32+
aliases: [],
33+
)
34+
}
35+
36+
type NextFunction(a) =
37+
fn(a) -> Configuration
38+
39+
fn or_default(in: Result(a, b), next: NextFunction(a)) -> Configuration {
40+
case in {
41+
Ok(a) -> next(a)
42+
Error(_) -> default()
43+
}
44+
}
45+
46+
fn or_none(
47+
in: Result(a, b),
48+
next: NextFunction(option.Option(a)),
49+
) -> Configuration {
50+
case in {
51+
Ok(a) -> next(option.Some(a))
52+
Error(_) -> next(option.None)
53+
}
54+
}
55+
56+
fn or_empty(
57+
in: Result(List(a), b),
58+
next: NextFunction(List(a)),
59+
) -> Configuration {
60+
case in {
61+
Ok(a) -> next(a)
62+
Error(_) -> next([])
63+
}
64+
}
65+
66+
fn parse_alias(in: dict.Dict(String, tom.Toml)) -> option.Option(Alias) {
67+
let name = tom.get_string(in, ["name"]) |> option.from_result
68+
let alias_type = tom.get_string(in, ["type"]) |> option.from_result
69+
let comic_id = tom.get_int(in, ["id"]) |> option.from_result
70+
71+
case name, alias_type, comic_id {
72+
option.Some(name), option.Some("id"), option.Some(comic_id) -> {
73+
IdAlias(name, comic_id) |> option.Some
74+
}
75+
option.Some(name), option.Some("latest"), _ -> {
76+
LatestAlias(name) |> option.Some
77+
}
78+
option.Some(name), option.Some("random"), _ -> {
79+
RandomAlias(name) |> option.Some
80+
}
81+
_, _, _ -> option.None
82+
}
83+
}
84+
85+
pub fn get_configuration_file() -> ConfigFile {
86+
let assert Ok(path) =
87+
envoy.get("XDG_CONFIG_HOME")
88+
|> result.or(envoy.get("HOME") |> result.map(fn(s) { s <> "/.config" }))
89+
|> result.or(Ok("."))
90+
91+
path <> "/gleeter/config.toml"
92+
}
93+
94+
pub fn parse(in: ConfigFile) -> Configuration {
95+
use file_content <- or_default(simplifile.read(in))
96+
use parsed <- or_default(tom.parse(file_content))
97+
98+
use random_start <- or_none(tom.get_int(parsed, ["random_start"]))
99+
use max_cols <- or_none(tom.get_int(parsed, ["screen", "max_cols"]))
100+
use max_lines <- or_none(tom.get_int(parsed, ["screen", "max_lines"]))
101+
102+
use aliases <- or_empty(tom.get_array(parsed, ["alias"]))
103+
let aliases =
104+
list.map(aliases, fn(alias) {
105+
case tom.as_table(alias) {
106+
Ok(alias) -> parse_alias(alias)
107+
Error(_) -> option.None
108+
}
109+
})
110+
|> list.filter(option.is_some)
111+
|> list.map(fn(s) {
112+
// We are pretty sure this cannot fail
113+
let assert option.Some(alias) = s
114+
alias
115+
})
116+
117+
Configuration(random_start, max_cols, max_lines, aliases)
118+
}

0 commit comments

Comments
 (0)