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)]
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        /// Flake.nix paths to process. If empty, runs on current directory.
186        #[arg(trailing_var_arg = true, num_args = 0..)]
187        paths: Vec<std::path::PathBuf>,
188    },
189    /// Manually add a single follows declaration.
190    ///
191    /// Example: `flake-edit add-follow rust-overlay.nixpkgs nixpkgs`
192    ///
193    /// This creates: `rust-overlay.inputs.nixpkgs.follows = "nixpkgs";`
194    ///
195    /// Without arguments, starts an interactive selection.
196    #[clap(alias = "af")]
197    AddFollow {
198        /// The input path in dot notation (e.g., "rust-overlay.nixpkgs" means
199        /// the nixpkgs input of rust-overlay).
200        input: Option<String>,
201        /// The target input to follow (e.g., "nixpkgs").
202        target: Option<String>,
203    },
204    #[clap(hide = true)]
205    #[command(name = "completion")]
206    /// Meant for shell completions.
207    Completion {
208        #[arg(long)]
209        inputs: bool,
210        mode: CompletionMode,
211    },
212    /// Manage flake-edit configuration.
213    #[clap(alias = "cfg", arg_required_else_help = true)]
214    Config {
215        /// Output the default configuration to stdout.
216        #[arg(long)]
217        print_default: bool,
218        /// Show where configuration would be loaded from.
219        #[arg(long)]
220        path: bool,
221    },
222}
223
224#[derive(Debug, Clone, Default)]
225/// Which command should be completed
226pub enum CompletionMode {
227    #[default]
228    None,
229    Add,
230    Change,
231    Follow,
232}
233
234impl From<String> for CompletionMode {
235    fn from(value: String) -> Self {
236        use CompletionMode::*;
237        match value.to_lowercase().as_str() {
238            "add" => Add,
239            "change" => Change,
240            "follow" => Follow,
241            _ => None,
242        }
243    }
244}
245
246#[derive(Debug, Clone, Default)]
247pub enum ListFormat {
248    None,
249    Simple,
250    Toplevel,
251    #[default]
252    Detailed,
253    Raw,
254    Json,
255}
256
257impl From<String> for ListFormat {
258    fn from(value: String) -> Self {
259        use ListFormat::*;
260        match value.to_lowercase().as_str() {
261            "detailed" => Detailed,
262            "simple" => Simple,
263            "toplevel" => Toplevel,
264            "raw" => Raw,
265            "json" => Json,
266            _ => None,
267        }
268    }
269}
270
271impl Display for ListFormat {
272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273        match self {
274            ListFormat::None => write!(f, ""),
275            ListFormat::Simple => write!(f, "simple"),
276            ListFormat::Toplevel => write!(f, "toplevel"),
277            ListFormat::Detailed => write!(f, "detailed"),
278            ListFormat::Raw => write!(f, "raw"),
279            ListFormat::Json => write!(f, "json"),
280        }
281    }
282}