Gleam CLI client for Tangled (clone of aly.codes/tg)
1//// Configuration: flags > TG_* env > config.toml > defaults.
2
3import envoy
4import filepath
5import gleam/dict
6import gleam/list
7import gleam/option.{type Option, None, Some}
8import gleam/string
9import gtg/sys
10import simplifile
11import tom
12
13pub const default_appview = "https://bobbin.klbr.net"
14
15pub type Settings {
16 Settings(appview: String, account: Option(String), json: Bool)
17}
18
19pub type FlagSettings {
20 FlagSettings(
21 config_path: Option(String),
22 appview: Option(String),
23 account: Option(String),
24 json: Bool,
25 )
26}
27
28pub fn default_settings() -> Settings {
29 Settings(appview: default_appview, account: None, json: False)
30}
31
32/// Parse known root flags from argv (does not consume subcommand args).
33pub fn parse_flags(args: List(String)) -> FlagSettings {
34 parse_flags_loop(args, FlagSettings(None, None, None, False))
35}
36
37fn parse_flags_loop(args: List(String), flags: FlagSettings) -> FlagSettings {
38 case args {
39 [] -> flags
40 ["--", ..] -> flags
41 ["--json", ..rest] ->
42 parse_flags_loop(rest, FlagSettings(..flags, json: True))
43 ["--config", value, ..rest] ->
44 parse_flags_loop(rest, FlagSettings(..flags, config_path: Some(value)))
45 ["--appview", value, ..rest] ->
46 parse_flags_loop(rest, FlagSettings(..flags, appview: Some(value)))
47 ["--account", value, ..rest] ->
48 parse_flags_loop(rest, FlagSettings(..flags, account: Some(value)))
49 [arg, ..rest] -> {
50 case string.split_once(arg, "=") {
51 Ok(#("--config", v)) ->
52 parse_flags_loop(rest, FlagSettings(..flags, config_path: Some(v)))
53 Ok(#("--appview", v)) ->
54 parse_flags_loop(rest, FlagSettings(..flags, appview: Some(v)))
55 Ok(#("--account", v)) ->
56 parse_flags_loop(rest, FlagSettings(..flags, account: Some(v)))
57 _ -> parse_flags_loop(rest, flags)
58 }
59 }
60 }
61}
62
63/// Strip global flags from args, leaving command path + command flags/args.
64pub fn strip_global_flags(args: List(String)) -> List(String) {
65 strip_loop(args, [])
66}
67
68fn strip_loop(args: List(String), acc: List(String)) -> List(String) {
69 case args {
70 [] -> list.reverse(acc)
71 ["--"] -> list.reverse(acc)
72 ["--json", ..rest] -> strip_loop(rest, acc)
73 ["--config", _value, ..rest] -> strip_loop(rest, acc)
74 ["--appview", _value, ..rest] -> strip_loop(rest, acc)
75 ["--account", _value, ..rest] -> strip_loop(rest, acc)
76 [arg, ..rest] -> {
77 case string.starts_with(arg, "--config=")
78 || string.starts_with(arg, "--appview=")
79 || string.starts_with(arg, "--account=")
80 {
81 True -> strip_loop(rest, acc)
82 False -> strip_loop(rest, [arg, ..acc])
83 }
84 }
85 }
86}
87
88pub fn load(flags: FlagSettings) -> Settings {
89 let file_settings = read_config_file(flags.config_path)
90 let appview =
91 coalesce_string([
92 flags.appview,
93 env("TG_APPVIEW"),
94 file_settings.appview,
95 Some(default_appview),
96 ])
97 let account =
98 coalesce_opt([flags.account, env("TG_ACCOUNT"), file_settings.account])
99 Settings(appview:, account:, json: flags.json)
100}
101
102fn env(name: String) -> Option(String) {
103 case envoy.get(name) {
104 Ok(v) if v != "" -> Some(v)
105 _ -> None
106 }
107}
108
109fn coalesce_string(opts: List(Option(String))) -> String {
110 case opts {
111 [] -> default_appview
112 [Some(v), ..] if v != "" -> v
113 [_, ..rest] -> coalesce_string(rest)
114 }
115}
116
117fn coalesce_opt(opts: List(Option(String))) -> Option(String) {
118 case opts {
119 [] -> None
120 [Some(v), ..] if v != "" -> Some(v)
121 [_, ..rest] -> coalesce_opt(rest)
122 }
123}
124
125type FileSettings {
126 FileSettings(appview: Option(String), account: Option(String))
127}
128
129fn read_config_file(path: Option(String)) -> FileSettings {
130 let candidates = case path {
131 Some(p) -> [p]
132 None -> [
133 filepath.join(sys.config_dir(), "config.toml"),
134 // also accept tg config for interoperability
135 filepath.join(
136 case envoy.get("XDG_CONFIG_HOME") {
137 Ok(xdg) if xdg != "" -> filepath.join(xdg, "tg")
138 _ ->
139 case envoy.get("HOME") {
140 Ok(home) -> filepath.join(filepath.join(home, ".config"), "tg")
141 Error(_) -> ".config/tg"
142 }
143 },
144 "config.toml",
145 ),
146 ]
147 }
148 case list.find_map(candidates, read_one_config) {
149 Ok(s) -> s
150 Error(_) -> FileSettings(None, None)
151 }
152}
153
154fn read_one_config(path: String) -> Result(FileSettings, Nil) {
155 case simplifile.read(path) {
156 Error(_) -> Error(Nil)
157 Ok(body) -> {
158 case tom.parse(body) {
159 Error(_) -> Error(Nil)
160 Ok(doc) -> {
161 let appview = tom_string(doc, "appview")
162 let account = tom_string(doc, "account")
163 Ok(FileSettings(appview:, account:))
164 }
165 }
166 }
167 }
168}
169
170fn tom_string(
171 doc: dict.Dict(String, tom.Toml),
172 key: String,
173) -> Option(String) {
174 case dict.get(doc, key) {
175 Ok(tom.String(s)) -> Some(s)
176 _ -> None
177 }
178}