implemented nixos and home-manager nix modules for the flake

This commit is contained in:
2026-02-19 17:30:21 -05:00
parent 982d11f21b
commit 934c41714a
4 changed files with 283 additions and 136 deletions

View File

@@ -33,5 +33,12 @@
platforms = platforms.linux; 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;
};
};
} }

117
nix/hm-module.nix Normal file
View File

@@ -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
];
};
}

21
nix/module.nix Normal file
View File

@@ -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 ];
};
}

View File

@@ -1,7 +1,7 @@
#![allow( #![allow(
clippy::derivable_impls, clippy::derivable_impls,
clippy::tabs_in_doc_comments, clippy::tabs_in_doc_comments,
clippy::while_let_on_iterator clippy::while_let_on_iterator
)] )]
pub mod builtin; pub mod builtin;
pub mod expand; pub mod expand;
@@ -41,16 +41,16 @@ use state::{read_vars, write_vars};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct FernArgs { struct FernArgs {
script: Option<String>, script: Option<String>,
#[arg(short)] #[arg(short)]
command: Option<String>, command: Option<String>,
#[arg(trailing_var_arg = true)] #[arg(trailing_var_arg = true)]
script_args: Vec<String>, script_args: Vec<String>,
#[arg(long)] #[arg(long)]
version: bool, version: bool,
} }
/// Force evaluation of lazily-initialized values early in shell startup. /// 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` /// closure, which forces access to the variable table and causes its `LazyLock`
/// constructor to run. /// constructor to run.
fn kickstart_lazy_evals() { fn kickstart_lazy_evals() {
read_vars(|_| {}); read_vars(|_| {});
} }
fn main() -> ExitCode { fn main() -> ExitCode {
env_logger::init(); env_logger::init();
kickstart_lazy_evals(); kickstart_lazy_evals();
let args = FernArgs::parse(); let args = FernArgs::parse();
if args.version { if args.version {
println!("fern {}", env!("CARGO_PKG_VERSION")); println!("fern {}", env!("CARGO_PKG_VERSION"));
return ExitCode::SUCCESS; return ExitCode::SUCCESS;
} }
if let Err(e) = if let Some(path) = args.script { if let Err(e) = if let Some(path) = args.script {
run_script(path, args.script_args) run_script(path, args.script_args)
} else if let Some(cmd) = args.command { } else if let Some(cmd) = args.command {
exec_input(cmd, None, false) exec_input(cmd, None, false)
} else { } else {
fern_interactive() fern_interactive()
} { } {
eprintln!("fern: {e}"); eprintln!("fern: {e}");
}; };
if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit)) if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit))
&& let Err(e) = exec_input(trap, None, false) { && let Err(e) = exec_input(trap, None, false) {
eprintln!("fern: error running EXIT trap: {e}"); eprintln!("fern: error running EXIT trap: {e}");
} }
ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8) ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8)
} }
fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> { fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> {
let path = path.as_ref(); let path = path.as_ref();
if !path.is_file() { if !path.is_file() {
eprintln!("fern: Failed to open input file: {}", path.display()); eprintln!("fern: Failed to open input file: {}", path.display());
QUIT_CODE.store(1, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst);
return Err(ShErr::simple(ShErrKind::CleanExit(1), "input file not found")); return Err(ShErr::simple(ShErrKind::CleanExit(1), "input file not found"));
} }
let Ok(input) = fs::read_to_string(path) else { let Ok(input) = fs::read_to_string(path) else {
eprintln!("fern: Failed to read input file: {}", path.display()); eprintln!("fern: Failed to read input file: {}", path.display());
QUIT_CODE.store(1, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst);
return Err(ShErr::simple(ShErrKind::CleanExit(1), "failed to read input file")); 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())); write_vars(|v| v.cur_scope_mut().bpush_arg(path.to_string_lossy().to_string()));
for arg in args { for arg in args {
write_vars(|v| v.cur_scope_mut().bpush_arg(arg)) write_vars(|v| v.cur_scope_mut().bpush_arg(arg))
} }
exec_input(input, None, false) exec_input(input, None, false)
} }
fn fern_interactive() -> ShResult<()> { fn fern_interactive() -> ShResult<()> {
let _raw_mode = raw_mode(); // sets raw mode, restores termios on drop let _raw_mode = raw_mode(); // sets raw mode, restores termios on drop
sig_setup(); sig_setup();
if let Err(e) = source_rc() { if let Err(e) = source_rc() {
eprintln!("{e}"); eprintln!("{e}");
} }
// Create readline instance with initial prompt // Create readline instance with initial prompt
let mut readline = match FernVi::new(get_prompt().ok()) { let mut readline = match FernVi::new(get_prompt().ok()) {
Ok(rl) => rl, Ok(rl) => rl,
Err(e) => { Err(e) => {
eprintln!("Failed to initialize readline: {e}"); eprintln!("Failed to initialize readline: {e}");
QUIT_CODE.store(1, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst);
return Err(ShErr::simple(ShErrKind::CleanExit(1), "readline initialization failed")); return Err(ShErr::simple(ShErrKind::CleanExit(1), "readline initialization failed"));
} }
}; };
// Main poll loop // Main poll loop
loop { loop {
// Handle any pending signals // Handle any pending signals
while signals_pending() { while signals_pending() {
if let Err(e) = check_signals() { if let Err(e) = check_signals() {
match e.kind() { match e.kind() {
ShErrKind::ClearReadline => { ShErrKind::ClearReadline => {
// Ctrl+C - clear current input and show new prompt // Ctrl+C - clear current input and show new prompt
readline.reset(get_prompt().ok()); readline.reset(get_prompt().ok());
} }
ShErrKind::CleanExit(code) => { ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(()); return Ok(());
} }
_ => eprintln!("{e}"), _ => eprintln!("{e}"),
} }
} }
} }
readline.print_line()?; readline.print_line()?;
// Poll for stdin input // Poll for stdin input
let mut fds = [PollFd::new( let mut fds = [PollFd::new(
unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }, unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) },
PollFlags::POLLIN, PollFlags::POLLIN,
)]; )];
match poll(&mut fds, PollTimeout::MAX) { match poll(&mut fds, PollTimeout::MAX) {
Ok(_) => {} Ok(_) => {}
Err(Errno::EINTR) => { Err(Errno::EINTR) => {
// Interrupted by signal, loop back to handle it // Interrupted by signal, loop back to handle it
continue; continue;
} }
Err(e) => { Err(e) => {
eprintln!("poll error: {e}"); eprintln!("poll error: {e}");
break; break;
} }
} }
// Check if stdin has data // Check if stdin has data
if fds[0].revents().is_some_and(|r| r.contains(PollFlags::POLLIN)) { if fds[0].revents().is_some_and(|r| r.contains(PollFlags::POLLIN)) {
let mut buffer = [0u8; 1024]; let mut buffer = [0u8; 1024];
match read(STDIN_FILENO, &mut buffer) { match read(STDIN_FILENO, &mut buffer) {
Ok(0) => { Ok(0) => {
// EOF // EOF
break; break;
} }
Ok(n) => { Ok(n) => {
readline.feed_bytes(&buffer[..n]); readline.feed_bytes(&buffer[..n]);
} }
Err(Errno::EINTR) => { Err(Errno::EINTR) => {
// Interrupted, continue to handle signals // Interrupted, continue to handle signals
continue; continue;
} }
Err(e) => { Err(e) => {
eprintln!("read error: {e}"); eprintln!("read error: {e}");
break; break;
} }
} }
} }
// Process any available input // Process any available input
match readline.process_input() { match readline.process_input() {
Ok(ReadlineEvent::Line(input)) => { Ok(ReadlineEvent::Line(input)) => {
let start = Instant::now(); let start = Instant::now();
write_meta(|m| m.start_timer()); write_meta(|m| m.start_timer());
if let Err(e) = exec_input(input, None, true) { if let Err(e) = exec_input(input, None, true) {
match e.kind() { match e.kind() {
ShErrKind::CleanExit(code) => { ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(()); return Ok(());
} }
_ => eprintln!("{e}"), _ => eprintln!("{e}"),
} }
} }
let command_run_time = start.elapsed(); 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 // Reset for next command with fresh prompt
readline.reset(get_prompt().ok()); readline.reset(get_prompt().ok());
let real_end = start.elapsed(); let real_end = start.elapsed();
} log::info!("Total round trip time: {:.2?}", real_end);
Ok(ReadlineEvent::Eof) => { }
// Ctrl+D on empty line Ok(ReadlineEvent::Eof) => {
QUIT_CODE.store(0, Ordering::SeqCst); // Ctrl+D on empty line
return Ok(()); QUIT_CODE.store(0, Ordering::SeqCst);
} return Ok(());
Ok(ReadlineEvent::Pending) => { }
// No complete input yet, keep polling Ok(ReadlineEvent::Pending) => {
} // No complete input yet, keep polling
Err(e) => { }
match e.kind() { Err(e) => {
ShErrKind::CleanExit(code) => { match e.kind() {
QUIT_CODE.store(*code, Ordering::SeqCst); ShErrKind::CleanExit(code) => {
return Ok(()); QUIT_CODE.store(*code, Ordering::SeqCst);
} return Ok(());
_ => eprintln!("{e}"), }
} _ => eprintln!("{e}"),
} }
} }
} }
}
Ok(()) Ok(())
} }