From 9836c79feb0de3128ea54d2dff0987740b72c067 Mon Sep 17 00:00:00 2001 From: pagedmov Date: Thu, 19 Feb 2026 17:30:21 -0500 Subject: [PATCH] implemented nixos and home-manager nix modules for the flake --- flake.nix | 9 +- nix/hm-module.nix | 117 ++++++++++++++++++++ nix/module.nix | 21 ++++ src/main.rs | 272 +++++++++++++++++++++++----------------------- 4 files changed, 283 insertions(+), 136 deletions(-) create mode 100644 nix/hm-module.nix create mode 100644 nix/module.nix diff --git a/flake.nix b/flake.nix index 62606f2..fd96f92 100644 --- a/flake.nix +++ b/flake.nix @@ -33,5 +33,12 @@ platforms = platforms.linux; }; }; - }); + }) // { + nixosModules.fern = import ./nix/module.nix; + homeModules.fern = import ./nix/hm-module.nix; + + overlays.default = final: prev: { + fern = self.packages.${final.system}.default; + }; + }; } diff --git a/nix/hm-module.nix b/nix/hm-module.nix new file mode 100644 index 0000000..dcde1b0 --- /dev/null +++ b/nix/hm-module.nix @@ -0,0 +1,117 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.programs.fern; + boolToString = b: + if b then "true" else "false"; +in +{ + options.programs.fern = { + enable = lib.mkEnableOption "fern shell"; + + package = lib.mkOption { + type = lib.types.package; + default = pkgs.fern; + description = "The fern package to use"; + }; + + settings = { + dotGlob = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to include hidden files in glob patterns"; + }; + autocd = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to automatically change into directories when they are entered as commands"; + }; + historyIgnoresDupes = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to ignore duplicate entries in the command history"; + }; + maxHistoryEntries = lib.mkOption { + type = lib.types.int; + default = 1000; + description = "The maximum number of entries to keep in the command history"; + }; + interactiveComments = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to allow comments in interactive mode"; + }; + autoHistory = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to automatically add commands to the history as they are executed"; + }; + bellStyle = lib.mkOption { + type = lib.types.enum [ "none" "audible" "visible" ]; + default = "audible"; + description = "The style of bell to use for notifications and errors"; + }; + maxRecurseDepth = lib.mkOption { + type = lib.types.int; + default = 1000; + description = "The maximum depth to allow when recursively executing shell functions"; + }; + + promptPathSegments = lib.mkOption { + type = lib.types.int; + default = 4; + description = "The maximum number of path segments to show in the prompt"; + }; + completionLimit = lib.mkOption { + type = lib.types.int; + default = 1000; + description = "The maximum number of completion candidates to show before truncating the list"; + }; + syntaxHighlighting = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to enable syntax highlighting in the shell"; + }; + tabStop = lib.mkOption { + type = lib.types.int; + default = 4; + description = "The number of spaces to use for tab stop in the shell"; + }; + + extraPostConfig = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Additional configuration to append to the fern configuration file"; + }; + extraPreConfig = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Additional configuration to prepend to the fern configuration file"; + }; + }; + }; + + config = lib.mkIf cfg.enable { + home.packages = [ cfg.package ]; + + home.file.".fernrc".text = lib.concatLines [ + cfg.settings.extraPreConfig + lib.concatLines [ + "shopt core.dotglob=${boolToString cfg.settings.dotGlob}" + "shopt core.autocd=${boolToString cfg.settings.autocd}" + "shopt core.hist_ignore_dupes=${boolToString cfg.settings.historyIgnoresDupes}" + "shopt core.max_hist=${toString cfg.settings.maxHistoryEntries}" + "shopt core.interactive_comments=${boolToString cfg.settings.interactiveComments}" + "shopt core.auto_hist=${boolToString cfg.settings.autoHistory}" + "shopt core.bell_style=${cfg.settings.bellStyle}" + "shopt core.max_recurse_depth=${toString cfg.settings.maxRecurseDepth}" + + "shopt prompt.trunc_prompt_path=${toString cfg.settings.promptPathSegments}" + "shopt prompt.comp_limit=${toString cfg.settings.completionLimit}" + "shopt prompt.highlight=${boolToString cfg.settings.syntaxHighlighting}" + "shopt prompt.tab_stop=${toString cfg.settings.tabStop}" + ] + cfg.settings.extraPostConfig + ]; + }; +} diff --git a/nix/module.nix b/nix/module.nix new file mode 100644 index 0000000..a1871c1 --- /dev/null +++ b/nix/module.nix @@ -0,0 +1,21 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.programs.fern; +in +{ + options.programs.fern = { + enable = lib.mkEnableOption "fern shell"; + + package = lib.mkOption { + type = lib.types.package; + default = pkgs.fern; + description = "The fern package to use"; + }; + }; + + config = lib.mkIf cfg.enable { + environment.systemPackages = [ cfg.package ]; + environment.shells = [ cfg.package ]; + }; +} diff --git a/src/main.rs b/src/main.rs index d7e2df3..edd18eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ #![allow( - clippy::derivable_impls, - clippy::tabs_in_doc_comments, - clippy::while_let_on_iterator + clippy::derivable_impls, + clippy::tabs_in_doc_comments, + clippy::while_let_on_iterator )] pub mod builtin; pub mod expand; @@ -41,16 +41,16 @@ use state::{read_vars, write_vars}; #[derive(Parser, Debug)] struct FernArgs { - script: Option, + script: Option, #[arg(short)] command: Option, - #[arg(trailing_var_arg = true)] - script_args: Vec, + #[arg(trailing_var_arg = true)] + script_args: Vec, - #[arg(long)] - version: bool, + #[arg(long)] + version: bool, } /// Force evaluation of lazily-initialized values early in shell startup. @@ -64,176 +64,178 @@ struct FernArgs { /// closure, which forces access to the variable table and causes its `LazyLock` /// constructor to run. fn kickstart_lazy_evals() { - read_vars(|_| {}); + read_vars(|_| {}); } fn main() -> ExitCode { env_logger::init(); - kickstart_lazy_evals(); - let args = FernArgs::parse(); - if args.version { - println!("fern {}", env!("CARGO_PKG_VERSION")); - return ExitCode::SUCCESS; - } + kickstart_lazy_evals(); + let args = FernArgs::parse(); + if args.version { + println!("fern {}", env!("CARGO_PKG_VERSION")); + return ExitCode::SUCCESS; + } - if let Err(e) = if let Some(path) = args.script { - run_script(path, args.script_args) + if let Err(e) = if let Some(path) = args.script { + run_script(path, args.script_args) } else if let Some(cmd) = args.command { exec_input(cmd, None, false) - } else { - fern_interactive() - } { + } else { + fern_interactive() + } { eprintln!("fern: {e}"); }; if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit)) - && let Err(e) = exec_input(trap, None, false) { - eprintln!("fern: error running EXIT trap: {e}"); + && let Err(e) = exec_input(trap, None, false) { + eprintln!("fern: error running EXIT trap: {e}"); } ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8) } fn run_script>(path: P, args: Vec) -> ShResult<()> { - let path = path.as_ref(); - if !path.is_file() { - eprintln!("fern: Failed to open input file: {}", path.display()); + let path = path.as_ref(); + if !path.is_file() { + eprintln!("fern: Failed to open input file: {}", path.display()); QUIT_CODE.store(1, Ordering::SeqCst); return Err(ShErr::simple(ShErrKind::CleanExit(1), "input file not found")); - } - let Ok(input) = fs::read_to_string(path) else { - eprintln!("fern: Failed to read input file: {}", path.display()); + } + let Ok(input) = fs::read_to_string(path) else { + eprintln!("fern: Failed to read input file: {}", path.display()); QUIT_CODE.store(1, Ordering::SeqCst); return Err(ShErr::simple(ShErrKind::CleanExit(1), "failed to read input file")); - }; + }; - write_vars(|v| v.cur_scope_mut().bpush_arg(path.to_string_lossy().to_string())); - for arg in args { - write_vars(|v| v.cur_scope_mut().bpush_arg(arg)) - } + write_vars(|v| v.cur_scope_mut().bpush_arg(path.to_string_lossy().to_string())); + for arg in args { + write_vars(|v| v.cur_scope_mut().bpush_arg(arg)) + } exec_input(input, None, false) } fn fern_interactive() -> ShResult<()> { - let _raw_mode = raw_mode(); // sets raw mode, restores termios on drop - sig_setup(); + let _raw_mode = raw_mode(); // sets raw mode, restores termios on drop + sig_setup(); - if let Err(e) = source_rc() { - eprintln!("{e}"); - } + if let Err(e) = source_rc() { + eprintln!("{e}"); + } - // Create readline instance with initial prompt - let mut readline = match FernVi::new(get_prompt().ok()) { - Ok(rl) => rl, - Err(e) => { - eprintln!("Failed to initialize readline: {e}"); - QUIT_CODE.store(1, Ordering::SeqCst); + // Create readline instance with initial prompt + let mut readline = match FernVi::new(get_prompt().ok()) { + Ok(rl) => rl, + Err(e) => { + eprintln!("Failed to initialize readline: {e}"); + QUIT_CODE.store(1, Ordering::SeqCst); return Err(ShErr::simple(ShErrKind::CleanExit(1), "readline initialization failed")); - } - }; + } + }; - // Main poll loop - loop { - // Handle any pending signals - while signals_pending() { - if let Err(e) = check_signals() { - match e.kind() { - ShErrKind::ClearReadline => { - // Ctrl+C - clear current input and show new prompt - readline.reset(get_prompt().ok()); - } - ShErrKind::CleanExit(code) => { - QUIT_CODE.store(*code, Ordering::SeqCst); + // Main poll loop + loop { + // Handle any pending signals + while signals_pending() { + if let Err(e) = check_signals() { + match e.kind() { + ShErrKind::ClearReadline => { + // Ctrl+C - clear current input and show new prompt + readline.reset(get_prompt().ok()); + } + ShErrKind::CleanExit(code) => { + QUIT_CODE.store(*code, Ordering::SeqCst); return Ok(()); - } - _ => eprintln!("{e}"), - } - } - } + } + _ => eprintln!("{e}"), + } + } + } readline.print_line()?; - // Poll for stdin input - let mut fds = [PollFd::new( - unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }, - PollFlags::POLLIN, - )]; + // Poll for stdin input + let mut fds = [PollFd::new( + unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }, + PollFlags::POLLIN, + )]; - match poll(&mut fds, PollTimeout::MAX) { - Ok(_) => {} - Err(Errno::EINTR) => { - // Interrupted by signal, loop back to handle it - continue; - } - Err(e) => { - eprintln!("poll error: {e}"); + match poll(&mut fds, PollTimeout::MAX) { + Ok(_) => {} + Err(Errno::EINTR) => { + // Interrupted by signal, loop back to handle it + continue; + } + Err(e) => { + eprintln!("poll error: {e}"); break; - } - } + } + } - // Check if stdin has data - if fds[0].revents().is_some_and(|r| r.contains(PollFlags::POLLIN)) { - let mut buffer = [0u8; 1024]; - match read(STDIN_FILENO, &mut buffer) { - Ok(0) => { - // EOF - break; - } - Ok(n) => { - readline.feed_bytes(&buffer[..n]); - } - Err(Errno::EINTR) => { - // Interrupted, continue to handle signals - continue; - } - Err(e) => { - eprintln!("read error: {e}"); - break; - } - } - } + // Check if stdin has data + if fds[0].revents().is_some_and(|r| r.contains(PollFlags::POLLIN)) { + let mut buffer = [0u8; 1024]; + match read(STDIN_FILENO, &mut buffer) { + Ok(0) => { + // EOF + break; + } + Ok(n) => { + readline.feed_bytes(&buffer[..n]); + } + Err(Errno::EINTR) => { + // Interrupted, continue to handle signals + continue; + } + Err(e) => { + eprintln!("read error: {e}"); + break; + } + } + } - // Process any available input - match readline.process_input() { - Ok(ReadlineEvent::Line(input)) => { + // Process any available input + match readline.process_input() { + Ok(ReadlineEvent::Line(input)) => { let start = Instant::now(); - write_meta(|m| m.start_timer()); - if let Err(e) = exec_input(input, None, true) { - match e.kind() { - ShErrKind::CleanExit(code) => { - QUIT_CODE.store(*code, Ordering::SeqCst); - return Ok(()); - } - _ => eprintln!("{e}"), - } - } + write_meta(|m| m.start_timer()); + if let Err(e) = exec_input(input, None, true) { + match e.kind() { + ShErrKind::CleanExit(code) => { + QUIT_CODE.store(*code, Ordering::SeqCst); + return Ok(()); + } + _ => eprintln!("{e}"), + } + } let command_run_time = start.elapsed(); - write_meta(|m| m.stop_timer()); + log::info!("Command executed in {:.2?}", command_run_time); + write_meta(|m| m.stop_timer()); - // Reset for next command with fresh prompt - readline.reset(get_prompt().ok()); + // Reset for next command with fresh prompt + readline.reset(get_prompt().ok()); let real_end = start.elapsed(); - } - Ok(ReadlineEvent::Eof) => { - // Ctrl+D on empty line - QUIT_CODE.store(0, Ordering::SeqCst); - return Ok(()); - } - Ok(ReadlineEvent::Pending) => { - // No complete input yet, keep polling - } - Err(e) => { - match e.kind() { - ShErrKind::CleanExit(code) => { - QUIT_CODE.store(*code, Ordering::SeqCst); - return Ok(()); - } - _ => eprintln!("{e}"), - } - } - } - } + log::info!("Total round trip time: {:.2?}", real_end); + } + Ok(ReadlineEvent::Eof) => { + // Ctrl+D on empty line + QUIT_CODE.store(0, Ordering::SeqCst); + return Ok(()); + } + Ok(ReadlineEvent::Pending) => { + // No complete input yet, keep polling + } + Err(e) => { + match e.kind() { + ShErrKind::CleanExit(code) => { + QUIT_CODE.store(*code, Ordering::SeqCst); + return Ok(()); + } + _ => eprintln!("{e}"), + } + } + } + } Ok(()) }