mirror of
https://github.com/ChrisTitusTech/linutil.git
synced 2025-04-20 10:53:06 +01:00
Compare commits
4 Commits
046aaceec1
...
0efd7d7cd2
Author | SHA1 | Date | |
---|---|---|---|
|
0efd7d7cd2 | ||
|
c12ae4a8ef | ||
|
6b572b1ab4 | ||
|
2c0a6ba80e |
23
README.md
23
README.md
@ -27,25 +27,18 @@ curl -fsSL https://christitus.com/linuxdev | sh
|
||||
|
||||
### CLI arguments
|
||||
|
||||
Linutil supports various command-line arguments to customize its behavior. Here are some common arguments you can use:
|
||||
|
||||
- `-c, --config <CONFIG>` : Path to the configuration file.
|
||||
- `--override-validation` : Show all available options, disregarding compatibility checks (UNSAFE).
|
||||
- `--size-bypass` : Bypass the terminal size limit.
|
||||
- `-y, --skip-confirmation` : Skip confirmation prompt before executing commands.
|
||||
- `-t, --theme <THEME>` : Set the theme to use in the application [default: `default`] [possible values: `default`, `compatible`].
|
||||
- `-h, --help` : Print help.
|
||||
|
||||
For more detailed usage, run:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://christitus.com/linux | sh -s -- --help
|
||||
```
|
||||
View available options by running:
|
||||
|
||||
```bash
|
||||
linutil --help
|
||||
```
|
||||
|
||||
For installer options:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://christitus.com/linux | sh -s -- --help
|
||||
```
|
||||
|
||||
## ⬇️ Installation
|
||||
|
||||
Linutil is also available as a package in various repositories:
|
||||
@ -142,7 +135,7 @@ Docs are now [here](https://github.com/Chris-Titus-Docs/linutil-docs)
|
||||
|
||||
## 🏅 Thanks to All Contributors
|
||||
|
||||
Thank you to everyone who has contributed to the development of Linutil. Your efforts are greatly appreciated, and you’re helping make this tool better for everyone!
|
||||
Thank you to everyone who has contributed to the development of Linutil. Your efforts are greatly appreciated, and you're helping make this tool better for everyone!
|
||||
|
||||
[](https://github.com/ChrisTitusTech/linutil/graphs/contributors)
|
||||
|
||||
|
@ -120,7 +120,7 @@ enum EntryType {
|
||||
|
||||
impl Entry {
|
||||
fn is_supported(&self) -> bool {
|
||||
self.preconditions.as_deref().map_or(true, |preconditions| {
|
||||
self.preconditions.as_deref().is_none_or(|preconditions| {
|
||||
preconditions.iter().all(
|
||||
|Precondition {
|
||||
matches,
|
||||
|
@ -35,8 +35,8 @@ install_nerd_font() {
|
||||
FONT_URL="https://github.com/ryanoasis/nerd-fonts/releases/latest/download/Meslo.zip"
|
||||
FONT_INSTALLED=$(fc-list | grep -i "Meslo")
|
||||
|
||||
# Check if Meslo Nerd-font is already installed
|
||||
if [ -n "$FONT_INSTALLED" ]; then
|
||||
# Replace -n test with standard test
|
||||
if [ ! -z "$FONT_INSTALLED" ]; then
|
||||
printf "%b\n" "${GREEN}Meslo Nerd-fonts are already installed.${RC}"
|
||||
return 0
|
||||
fi
|
||||
@ -207,29 +207,37 @@ setupDisplayManager() {
|
||||
done
|
||||
printf "%b\n" "${GREEN}Current display manager: $currentdm${RC}"
|
||||
if [ "$currentdm" = "none" ]; then
|
||||
printf "%b\n" "${YELLOW}--------------------------${RC}"
|
||||
printf "%b\n" "${YELLOW}Pick your Display Manager ${RC}"
|
||||
printf "%b\n" "${YELLOW}1. SDDM ${RC}"
|
||||
printf "%b\n" "${YELLOW}2. LightDM ${RC}"
|
||||
printf "%b\n" "${YELLOW}3. GDM ${RC}"
|
||||
printf "%b\n" "${YELLOW} ${RC}"
|
||||
printf "%b" "${YELLOW}Please select one: ${RC}"
|
||||
read -r choice
|
||||
case "$choice" in
|
||||
1)
|
||||
DM="sddm"
|
||||
;;
|
||||
2)
|
||||
DM="lightdm"
|
||||
;;
|
||||
3)
|
||||
DM="gdm"
|
||||
;;
|
||||
*)
|
||||
printf "%b\n" "${RED}Invalid selection! Please choose 1, 2, or 3.${RC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
while : ; do
|
||||
printf "%b\n" "${YELLOW}--------------------------${RC}"
|
||||
printf "%b\n" "${YELLOW}Pick your Display Manager ${RC}"
|
||||
printf "%b\n" "${YELLOW}1. SDDM ${RC}"
|
||||
printf "%b\n" "${YELLOW}2. LightDM ${RC}"
|
||||
printf "%b\n" "${YELLOW}3. GDM ${RC}"
|
||||
printf "%b\n" "${YELLOW}4. None ${RC}"
|
||||
printf "%b" "${YELLOW}Please select one: ${RC}"
|
||||
read -r choice
|
||||
case "$choice" in
|
||||
1)
|
||||
DM="sddm"
|
||||
break
|
||||
;;
|
||||
2)
|
||||
DM="lightdm"
|
||||
break
|
||||
;;
|
||||
3)
|
||||
DM="gdm"
|
||||
break
|
||||
;;
|
||||
4)
|
||||
printf "%b\n" "${GREEN}No display manager will be installed${RC}"
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
printf "%b\n" "${RED}Invalid selection! Please choose 1, 2, 3, or 4.${RC}"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
case "$PACKAGER" in
|
||||
pacman)
|
||||
"$ESCALATION_TOOL" "$PACKAGER" -S --needed --noconfirm "$DM"
|
||||
|
1
rustfmt.toml
Normal file
1
rustfmt.toml
Normal file
@ -0,0 +1 @@
|
||||
imports_granularity = "Crate"
|
@ -14,7 +14,7 @@ default = ["tips"]
|
||||
tips = ["rand"]
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.20", features = ["derive", "std"], default-features = false }
|
||||
clap = { version = "4.5.20", features = ["derive", "std", "help", "usage"], default-features = false }
|
||||
oneshot = { version = "0.1.8", features = ["std"], default-features = false }
|
||||
portable-pty = "0.8.1"
|
||||
ratatui = { version = "0.29.0", features = ["crossterm"], default-features = false }
|
||||
|
35
tui/src/cli.rs
Normal file
35
tui/src/cli.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use crate::theme::Theme;
|
||||
use clap::Parser;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Parser, Clone)]
|
||||
pub struct Args {
|
||||
/// Path to the configuration file
|
||||
#[arg(short, long)]
|
||||
pub config: Option<PathBuf>,
|
||||
|
||||
/// Set the theme to use in the application
|
||||
#[arg(short, long, value_enum)]
|
||||
#[arg(default_value_t = Theme::Default)]
|
||||
pub theme: Theme,
|
||||
|
||||
/// Skip confirmation prompt before executing commands
|
||||
#[arg(short = 'y', long)]
|
||||
pub skip_confirmation: bool,
|
||||
|
||||
/// Show all available options, disregarding compatibility checks (UNSAFE)
|
||||
#[arg(short = 'u', long)]
|
||||
pub override_validation: bool,
|
||||
|
||||
/// Bypass the terminal size limit
|
||||
#[arg(short = 's', long)]
|
||||
pub size_bypass: bool,
|
||||
|
||||
/// Enable mouse interaction
|
||||
#[arg(short = 'm', long)]
|
||||
pub mouse: bool,
|
||||
|
||||
/// Bypass root user check
|
||||
#[arg(short = 'r', long)]
|
||||
pub bypass_root: bool,
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
mod cli;
|
||||
mod confirmation;
|
||||
mod filter;
|
||||
mod float;
|
||||
@ -11,7 +12,7 @@ mod theme;
|
||||
#[cfg(feature = "tips")]
|
||||
mod tips;
|
||||
|
||||
use crate::theme::Theme;
|
||||
use crate::cli::Args;
|
||||
use clap::Parser;
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
@ -26,40 +27,17 @@ use ratatui::{
|
||||
use state::AppState;
|
||||
use std::{
|
||||
io::{stdout, Result, Stdout},
|
||||
path::PathBuf,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
// Linux utility toolbox
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Args {
|
||||
#[arg(short, long, help = "Path to the configuration file")]
|
||||
config: Option<PathBuf>,
|
||||
#[arg(short, long, value_enum)]
|
||||
#[arg(default_value_t = Theme::Default)]
|
||||
#[arg(help = "Set the theme to use in the application")]
|
||||
theme: Theme,
|
||||
#[arg(
|
||||
short = 'y',
|
||||
long,
|
||||
help = "Skip confirmation prompt before executing commands"
|
||||
)]
|
||||
skip_confirmation: bool,
|
||||
#[arg(long, default_value_t = false)]
|
||||
#[clap(help = "Show all available options, disregarding compatibility checks (UNSAFE)")]
|
||||
override_validation: bool,
|
||||
#[arg(long, default_value_t = false)]
|
||||
#[clap(help = "Bypass the terminal size limit")]
|
||||
size_bypass: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
let mut state = AppState::new(args);
|
||||
let mut state = AppState::new(args.clone());
|
||||
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
stdout().execute(EnableMouseCapture)?;
|
||||
if args.mouse {
|
||||
stdout().execute(EnableMouseCapture)?;
|
||||
}
|
||||
|
||||
enable_raw_mode()?;
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||
@ -70,7 +48,9 @@ fn main() -> Result<()> {
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
terminal.backend_mut().execute(LeaveAlternateScreen)?;
|
||||
terminal.backend_mut().execute(DisableMouseCapture)?;
|
||||
if args.mouse {
|
||||
terminal.backend_mut().execute(DisableMouseCapture)?;
|
||||
}
|
||||
terminal.backend_mut().execute(ResetColor)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
|
@ -8,8 +8,12 @@ This means you have full system access and commands can potentially damage your
|
||||
Please proceed with caution and make sure you understand what each script does before executing it.";
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn check_root_status<'a>() -> Option<FloatingText<'a>> {
|
||||
(Uid::effective().is_root()).then_some(FloatingText::new(
|
||||
pub fn check_root_status(bypass_root: bool) -> Option<FloatingText<'static>> {
|
||||
if bypass_root {
|
||||
return None;
|
||||
}
|
||||
|
||||
Uid::effective().is_root().then_some(FloatingText::new(
|
||||
ROOT_WARNING.into(),
|
||||
"Root User Warning",
|
||||
true,
|
||||
|
@ -66,6 +66,7 @@ pub struct AppState {
|
||||
tip: &'static str,
|
||||
size_bypass: bool,
|
||||
skip_confirmation: bool,
|
||||
mouse_enabled: bool,
|
||||
}
|
||||
|
||||
pub enum Focus {
|
||||
@ -94,8 +95,18 @@ enum SelectedItem {
|
||||
None,
|
||||
}
|
||||
|
||||
enum ScrollDir {
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(args: Args) -> Self {
|
||||
#[cfg(unix)]
|
||||
let root_warning = check_root_status(args.bypass_root);
|
||||
#[cfg(not(unix))]
|
||||
let root_warning = None;
|
||||
|
||||
let tabs = linutil_core::get_tabs(!args.override_validation);
|
||||
let root_id = tabs[0].tree.root().id();
|
||||
|
||||
@ -122,10 +133,11 @@ impl AppState {
|
||||
tip: crate::tips::get_random_tip(),
|
||||
size_bypass: args.size_bypass,
|
||||
skip_confirmation: args.skip_confirmation,
|
||||
mouse_enabled: args.mouse,
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
if let Some(root_warning) = check_root_status() {
|
||||
if let Some(root_warning) = root_warning {
|
||||
state.spawn_float(root_warning, FLOAT_SIZE, FLOAT_SIZE);
|
||||
}
|
||||
|
||||
@ -333,16 +345,24 @@ impl AppState {
|
||||
.map(|tab| tab.name.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let tab_hl_style = if let Focus::TabList = self.focus {
|
||||
Style::default().reversed().fg(self.theme.tab_color())
|
||||
let (tab_hl_style, highlight_symbol) = if let Focus::TabList = self.focus {
|
||||
(
|
||||
Style::default().reversed().fg(self.theme.tab_color()),
|
||||
self.theme.tab_icon(),
|
||||
)
|
||||
} else if let Focus::Search = self.focus {
|
||||
(Style::reset(), " ")
|
||||
} else {
|
||||
Style::new().fg(self.theme.tab_color())
|
||||
(
|
||||
Style::new().fg(self.theme.tab_color()),
|
||||
self.theme.tab_icon(),
|
||||
)
|
||||
};
|
||||
|
||||
let tab_list = List::new(tabs)
|
||||
.block(Block::bordered().border_set(border::ROUNDED))
|
||||
.highlight_style(tab_hl_style)
|
||||
.highlight_symbol(self.theme.tab_icon());
|
||||
.highlight_symbol(highlight_symbol);
|
||||
frame.render_stateful_widget(tab_list, left_chunks[1], &mut self.current_tab);
|
||||
|
||||
let chunks =
|
||||
@ -440,6 +460,10 @@ impl AppState {
|
||||
}
|
||||
|
||||
pub fn handle_mouse(&mut self, event: &MouseEvent) -> bool {
|
||||
if !self.mouse_enabled {
|
||||
return true;
|
||||
}
|
||||
|
||||
if !self.drawable {
|
||||
return true;
|
||||
}
|
||||
@ -597,24 +621,35 @@ impl AppState {
|
||||
true
|
||||
}
|
||||
|
||||
fn scroll_down(&mut self) {
|
||||
if let Some(selected) = self.selection.selected() {
|
||||
if selected == self.filter.item_list().len() - 1 {
|
||||
self.selection.select_first();
|
||||
} else {
|
||||
self.selection.select_next();
|
||||
}
|
||||
}
|
||||
fn scroll(&mut self, direction: ScrollDir) {
|
||||
let Some(selected) = self.selection.selected() else {
|
||||
return;
|
||||
};
|
||||
let list_len = if !self.at_root() {
|
||||
self.filter.item_list().len() + 1
|
||||
} else {
|
||||
self.filter.item_list().len()
|
||||
};
|
||||
|
||||
if list_len == 0 {
|
||||
return;
|
||||
};
|
||||
|
||||
let next_selection = match direction {
|
||||
ScrollDir::Up if selected == 0 => list_len - 1,
|
||||
ScrollDir::Down if selected >= list_len - 1 => 0,
|
||||
ScrollDir::Up => selected - 1,
|
||||
ScrollDir::Down => selected + 1,
|
||||
};
|
||||
self.selection.select(Some(next_selection));
|
||||
}
|
||||
|
||||
fn scroll_up(&mut self) {
|
||||
if let Some(selected) = self.selection.selected() {
|
||||
if selected == 0 {
|
||||
self.selection.select_last();
|
||||
} else {
|
||||
self.selection.select_previous();
|
||||
}
|
||||
}
|
||||
self.scroll(ScrollDir::Up)
|
||||
}
|
||||
|
||||
fn scroll_down(&mut self) {
|
||||
self.scroll(ScrollDir::Down)
|
||||
}
|
||||
|
||||
fn toggle_multi_select(&mut self) {
|
||||
|
@ -2,8 +2,7 @@ use std::fs;
|
||||
|
||||
use linutil_core::Command;
|
||||
|
||||
use crate::path;
|
||||
use crate::DynError;
|
||||
use crate::{path, DynError};
|
||||
|
||||
pub const USER_GUIDE: &str = "userguide.md";
|
||||
|
||||
|
@ -6,9 +6,10 @@ use std::{env, error::Error};
|
||||
type DynError = Box<dyn Error>;
|
||||
|
||||
pub mod tasks {
|
||||
use crate::docgen::USER_GUIDE;
|
||||
use crate::docgen::{userguide, write};
|
||||
use crate::DynError;
|
||||
use crate::{
|
||||
docgen::{userguide, write, USER_GUIDE},
|
||||
DynError,
|
||||
};
|
||||
|
||||
pub fn docgen() -> Result<(), DynError> {
|
||||
write(USER_GUIDE, &userguide()?);
|
||||
|
Loading…
x
Reference in New Issue
Block a user