Skip to main content

flake_edit/
cli.rs

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