implemented nixos and home-manager nix modules for the flake
This commit is contained in:
@@ -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
117
nix/hm-module.nix
Normal 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
21
nix/module.nix
Normal 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 ];
|
||||||
|
};
|
||||||
|
}
|
||||||
272
src/main.rs
272
src/main.rs
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user