1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
use std::fmt::Display;
use clap::{Parser, Subcommand};
#[derive(Parser, Debug)]
#[command(author, version = CliArgs::unstable_version(), about, long_about = None)]
#[command(name = "flake-edit")]
#[command(next_line_help = true)]
/// Edit your flake inputs with ease
pub struct CliArgs {
/// Location of the `flake.nix` file, that will be used.
/// Defaults to `flake.nix` in the current directory.
#[arg(long)]
flake: Option<String>,
/// Location of the `flake.lock` file.
/// Defaults to `flake.lock` in the current directory.
#[arg(long)]
lock_file: Option<String>,
/// Print a diff of the changes, will not write the changes to disk.
#[arg(long, default_value_t = false)]
diff: bool,
/// Skip updating the lockfile after editing flake.nix.
#[arg(long, default_value_t = false)]
no_lock: bool,
/// Disable interactive prompts.
#[arg(long, default_value_t = false)]
non_interactive: bool,
/// Disable reading from and writing to the completion cache.
#[arg(long, default_value_t = false)]
no_cache: bool,
/// Path to a custom cache file.
#[arg(long)]
cache: Option<String>,
/// Path to a custom configuration file.
#[arg(long, global = true)]
config: Option<String>,
#[command(subcommand)]
subcommand: Command,
}
#[allow(unused)]
impl CliArgs {
/// Surface current version together with the current git revision and date, if available
fn unstable_version() -> &'static str {
const VERSION: &str = env!("CARGO_PKG_VERSION");
let date = option_env!("GIT_DATE").unwrap_or("no_date");
let rev = option_env!("GIT_REV").unwrap_or("no_rev");
// This is a memory leak, only use sparingly.
Box::leak(format!("{VERSION} - {date} - {rev}").into_boxed_str())
}
pub fn subcommand(&self) -> &Command {
&self.subcommand
}
pub fn list(&self) -> bool {
matches!(self.subcommand, Command::List { .. })
}
pub fn update(&self) -> bool {
matches!(self.subcommand, Command::Update { .. })
}
pub fn pin(&self) -> bool {
matches!(self.subcommand, Command::Pin { .. })
}
pub fn unpin(&self) -> bool {
matches!(self.subcommand, Command::Unpin { .. })
}
pub fn change(&self) -> bool {
matches!(self.subcommand, Command::Change { .. })
}
pub fn follow(&self) -> bool {
matches!(self.subcommand, Command::Follow { .. })
}
pub fn flake(&self) -> Option<&String> {
self.flake.as_ref()
}
pub fn lock_file(&self) -> Option<&String> {
self.lock_file.as_ref()
}
pub fn diff(&self) -> bool {
self.diff
}
pub fn no_lock(&self) -> bool {
self.no_lock
}
pub fn non_interactive(&self) -> bool {
self.non_interactive
}
pub fn no_cache(&self) -> bool {
self.no_cache
}
pub fn cache(&self) -> Option<&String> {
self.cache.as_ref()
}
pub fn config(&self) -> Option<&String> {
self.config.as_ref()
}
}
#[derive(Subcommand, Debug)]
pub enum Command {
/// Add a new flake reference.
#[clap(alias = "a")]
Add {
/// The name of an input attribute.
id: Option<String>,
/// The uri that should be added to the input.
uri: Option<String>,
#[arg(long)]
/// Pin to a specific ref_or_rev
ref_or_rev: Option<String>,
/// The input itself is not a flake.
#[arg(long, short)]
no_flake: bool,
/// Use shallow clone for the input.
#[arg(long, short)]
shallow: bool,
},
/// Remove a specific flake reference based on its id.
#[clap(alias = "rm")]
Remove { id: Option<String> },
/// Change an existing flake reference's URI.
#[clap(alias = "c")]
Change {
/// The name of an existing input attribute.
id: Option<String>,
/// The new URI for the input.
uri: Option<String>,
#[arg(long)]
/// Pin to a specific ref_or_rev
ref_or_rev: Option<String>,
/// Use shallow clone for the input.
#[arg(long, short)]
shallow: bool,
},
/// List flake inputs
#[clap(alias = "l")]
List {
#[arg(long, default_value_t = ListFormat::default())]
format: ListFormat,
},
/// Update inputs to their latest specified release.
#[clap(alias = "u")]
Update {
/// The id of an input attribute.
/// If omitted will update all inputs.
id: Option<String>,
/// Whether the latest semver release of the remote should be used even thought the release
/// itself isn't yet pinned to a specific release.
#[arg(long)]
init: bool,
},
/// Pin inputs to their current or a specified rev.
#[clap(alias = "p")]
Pin {
/// The id of an input attribute.
id: Option<String>,
/// Optionally specify a rev for the inputs attribute.
rev: Option<String>,
},
/// Unpin an input so it tracks the upstream default again.
#[clap(alias = "up")]
Unpin {
/// The id of an input attribute.
id: Option<String>,
},
/// Automatically add and remove follows declarations.
///
/// Analyzes the flake.lock to find nested inputs that match top-level inputs,
/// then adds appropriate follows declarations and removes stale ones.
///
/// With file paths, processes multiple flakes in batch.
/// For every `flake.nix` file passed in it will assume a
/// `flake.lock` file exists in the same directory.
#[clap(alias = "f")]
Follow {
/// Enable transitive follows deduplication, promoting shared nested
/// inputs to top-level when they appear at least N times.
/// Defaults to 2 if no value is given. Overrides the config file's
/// `follow.transitive_min`.
#[arg(long, num_args = 0..=1, default_missing_value = "2")]
transitive: Option<usize>,
/// Flake.nix paths to process. If empty, runs on current directory.
#[arg(trailing_var_arg = true, num_args = 0..)]
paths: Vec<std::path::PathBuf>,
},
/// Manually add a single follows declaration.
///
/// Example: `flake-edit add-follow rust-overlay.nixpkgs nixpkgs`
///
/// This creates: `rust-overlay.inputs.nixpkgs.follows = "nixpkgs";`
///
/// Without arguments, starts an interactive selection.
#[clap(alias = "af")]
AddFollow {
/// The input path in dot notation (e.g., "rust-overlay.nixpkgs" means
/// the nixpkgs input of rust-overlay).
input: Option<String>,
/// The target input to follow (e.g., "nixpkgs").
target: Option<String>,
},
#[clap(hide = true)]
#[command(name = "completion")]
/// Meant for shell completions.
Completion {
#[arg(long)]
inputs: bool,
mode: CompletionMode,
},
/// Manage flake-edit configuration.
#[clap(alias = "cfg", arg_required_else_help = true)]
Config {
/// Output the default configuration to stdout.
#[arg(long)]
print_default: bool,
/// Show where configuration would be loaded from.
#[arg(long)]
path: bool,
},
}
#[derive(Debug, Clone, Default)]
/// Which command should be completed
pub enum CompletionMode {
#[default]
None,
Add,
Change,
Follow,
}
impl From<String> for CompletionMode {
fn from(value: String) -> Self {
use CompletionMode::*;
match value.to_lowercase().as_str() {
"add" => Add,
"change" => Change,
"follow" => Follow,
_ => None,
}
}
}
#[derive(Debug, Clone, Default)]
pub enum ListFormat {
None,
Simple,
Toplevel,
#[default]
Detailed,
Raw,
Json,
}
impl From<String> for ListFormat {
fn from(value: String) -> Self {
use ListFormat::*;
match value.to_lowercase().as_str() {
"detailed" => Detailed,
"simple" => Simple,
"toplevel" => Toplevel,
"raw" => Raw,
"json" => Json,
_ => None,
}
}
}
impl Display for ListFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ListFormat::None => write!(f, ""),
ListFormat::Simple => write!(f, "simple"),
ListFormat::Toplevel => write!(f, "toplevel"),
ListFormat::Detailed => write!(f, "detailed"),
ListFormat::Raw => write!(f, "raw"),
ListFormat::Json => write!(f, "json"),
}
}
}