diff --git a/Cargo.lock b/Cargo.lock index d0a8bdca..02e81e5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,6 +280,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "hashbrown" version = "0.14.5" @@ -319,6 +325,25 @@ dependencies = [ "cc", ] +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "ioctl-rs" version = "0.1.6" @@ -525,6 +550,34 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + [[package]] name = "ratatui" version = "0.27.0" @@ -546,6 +599,15 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "redox_syscall" version = "0.5.1" @@ -555,6 +617,15 @@ dependencies = [ "bitflags 2.5.0", ] +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "rustversion" version = "1.0.17" @@ -722,6 +793,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand", + "remove_dir_all", +] + [[package]] name = "termios" version = "0.2.2" @@ -759,9 +840,11 @@ dependencies = [ "clap", "crossterm", "ego-tree", + "include_dir", "oneshot", "portable-pty", "ratatui", + "tempdir", "tui-term", ] diff --git a/Cargo.toml b/Cargo.toml index 32876ac6..fa2dffae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,9 @@ oneshot = "0.1.8" portable-pty = "0.8.1" ratatui = "0.27.0" tui-term = "0.1.12" +include_dir = "0.7.4" +tempdir = "0.3.7" [[bin]] name = "linutil" -path = "src/main.rs" \ No newline at end of file +path = "src/main.rs" diff --git a/build/release/linutil b/build/release/linutil index b66d11fb..93077bb0 100755 Binary files a/build/release/linutil and b/build/release/linutil differ diff --git a/src/commands/dotfiles/alacritty-setup.sh b/src/commands/dotfiles/alacritty-setup.sh index 2b363e85..46143729 100755 --- a/src/commands/dotfiles/alacritty-setup.sh +++ b/src/commands/dotfiles/alacritty-setup.sh @@ -1,5 +1,7 @@ #!/bin/sh -e +. ./common-script.sh + setupAlacritty() { echo "Install Alacritty if not already installed..." if ! command_exists alacritty; then diff --git a/src/commands/dotfiles/kitty-setup.sh b/src/commands/dotfiles/kitty-setup.sh index bad29ac0..b2bce9ea 100755 --- a/src/commands/dotfiles/kitty-setup.sh +++ b/src/commands/dotfiles/kitty-setup.sh @@ -1,5 +1,7 @@ #!/bin/sh -e +. ./common-script.sh + setupKitty() { echo "Install Kitty if not already installed..." if ! command_exists kitty; then diff --git a/src/commands/dotfiles/rofi-setup.sh b/src/commands/dotfiles/rofi-setup.sh index ea2be0f2..a6639bb9 100755 --- a/src/commands/dotfiles/rofi-setup.sh +++ b/src/commands/dotfiles/rofi-setup.sh @@ -1,5 +1,7 @@ #!/bin/sh -e +. ./common-script.sh + setupRofi() { echo "Install Rofi if not already installed..." if ! command_exists rofi; then diff --git a/src/commands/system-setup/1-compile-setup.sh b/src/commands/system-setup/1-compile-setup.sh index 345f09c8..8f96aa8f 100755 --- a/src/commands/system-setup/1-compile-setup.sh +++ b/src/commands/system-setup/1-compile-setup.sh @@ -1,5 +1,7 @@ #!/bin/sh -e +. ./common-script.sh + # Check if the home directory and linuxtoolbox folder exist, create them if they don't LINUXTOOLBOXDIR="$HOME/linuxtoolbox" diff --git a/src/commands/system-setup/2-gaming-setup.sh b/src/commands/system-setup/2-gaming-setup.sh index 1d903105..96bfe777 100755 --- a/src/commands/system-setup/2-gaming-setup.sh +++ b/src/commands/system-setup/2-gaming-setup.sh @@ -1,5 +1,7 @@ #!/bin/sh -e +. ./common-script.sh + installDepend() { ## Check for dependencies. echo -e "${YELLOW}Installing dependencies...${RC}" diff --git a/src/commands/system-setup/3-global-theme.sh b/src/commands/system-setup/3-global-theme.sh index a1ad4824..eaebcd6e 100644 --- a/src/commands/system-setup/3-global-theme.sh +++ b/src/commands/system-setup/3-global-theme.sh @@ -1,5 +1,7 @@ #!/bin/sh -e +. ./common-script.sh + # Check if the home directory and linuxtoolbox folder exist, create them if they don't LINUXTOOLBOXDIR="$HOME/linuxtoolbox" @@ -71,4 +73,4 @@ EOF checkEnv install_theme_tools configure_qt6ct -configure_kvantum \ No newline at end of file +configure_kvantum diff --git a/src/commands/system-update.sh b/src/commands/system-update.sh index b86172b8..78393f70 100755 --- a/src/commands/system-update.sh +++ b/src/commands/system-update.sh @@ -1,5 +1,7 @@ #!/bin/sh -e +. ./common-script.sh + fastUpdate() { case ${PACKAGER} in pacman) diff --git a/src/commands/test/lib.sh b/src/commands/test/lib.sh new file mode 100755 index 00000000..45e08b7a --- /dev/null +++ b/src/commands/test/lib.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +say_hello () { + echo Hi +} diff --git a/src/commands/test/main.sh b/src/commands/test/main.sh new file mode 100755 index 00000000..7af16b96 --- /dev/null +++ b/src/commands/test/main.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +# The current working directory will always be inside "commands" +. test/lib.sh + +say_hello diff --git a/src/list.rs b/src/list.rs index b564bdea..1b22f762 100644 --- a/src/list.rs +++ b/src/list.rs @@ -1,4 +1,4 @@ -use crate::{float::floating_window, theme::*}; +use crate::{float::floating_window, running_command::Command, state::AppState}; use crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; use ego_tree::{tree, NodeId}; use ratatui::{ @@ -9,18 +9,9 @@ use ratatui::{ Frame, }; -macro_rules! with_common_script { - ($command:expr) => { - concat!( - include_str!("commands/common-script.sh"), - include_str!($command) - ) - }; -} - struct ListNode { name: &'static str, - command: &'static str, + command: Command, } /// This is a data structure that has everything necessary to draw and manage a menu of commands @@ -60,60 +51,74 @@ impl CustomList { // case the tree! macro expands to `ego-tree::tree` data structure let tree = tree!(ListNode { name: "root", - command: "" + command: Command::None, } => { ListNode { name: "Full System Update", - command: with_common_script!("commands/system-update.sh"), + command: Command::LocalFile("system-update.sh"), }, ListNode { name: "Setup Bash Prompt", - command: "bash -c \"$(curl -s https://raw.githubusercontent.com/ChrisTitusTech/mybash/main/setup.sh)\"" + command: Command::Raw("bash -c \"$(curl -s https://raw.githubusercontent.com/ChrisTitusTech/mybash/main/setup.sh)\""), }, ListNode { name: "Setup Neovim", - command: "bash -c \"$(curl -s https://raw.githubusercontent.com/ChrisTitusTech/neovim/main/setup.sh)\"" + command: Command::Raw("bash -c \"$(curl -s https://raw.githubusercontent.com/ChrisTitusTech/neovim/main/setup.sh)\""), }, - // ListNode { - // name: "Just ls, nothing special, trust me", - // command: include_str!("commands/special_ls.sh"), - // }, ListNode { name: "System Setup", - command: "" + command: Command::None, } => { ListNode { name: "Build Prerequisites", - command: with_common_script!("commands/system-setup/1-compile-setup.sh"), + command: Command::LocalFile("system-setup/1-compile-setup.sh"), }, ListNode { name: "Gaming Dependencies", - command: with_common_script!("commands/system-setup/2-gaming-setup.sh"), + command: Command::LocalFile("system-setup/2-gaming-setup.sh"), }, ListNode { name: "Global Theme", - command: with_common_script!("commands/system-setup/3-global-theme.sh"), + command: Command::LocalFile("system-setup/3-global-theme.sh"), }, - ListNode { - name: "Recursion?", - command: "cargo run" - } }, ListNode { name: "Titus Dotfiles", - command: "" + command: Command::None } => { ListNode { name: "Alacritty Setup", - command: with_common_script!("commands/dotfiles/alacritty-setup.sh"), + command: Command::LocalFile("dotfiles/alacritty-setup.sh"), }, ListNode { name: "Kitty Setup", - command: with_common_script!("commands/dotfiles/kitty-setup.sh"), + command: Command::LocalFile("dotfiles/kitty-setup.sh"), }, ListNode { name: "Rofi Setup", - command: with_common_script!("commands/dotfiles/rofi-setup.sh"), + command: Command::LocalFile("dotfiles/rofi-setup.sh"), + }, + }, + + ListNode { + name: "Testing category", + command: Command::None, + } => { + ListNode { + name: "Complex command", + command: Command::Raw("sleep 1 && ls -la && sleep 1 && ls -la && echo Bonus eza comming... && sleep 1 && ls -la"), + }, + ListNode { + name: "Neovim", + command: Command::Raw("nvim"), + }, + ListNode { + name: "Full bash", + command: Command::Raw("bash"), + }, + ListNode { + name: "Running file with `source`", + command: Command::LocalFile("test/main.sh"), }, } }); @@ -130,9 +135,8 @@ impl CustomList { } /// Draw our custom widget to the frame - pub fn draw(&mut self, frame: &mut Frame, area: Rect) { + pub fn draw(&mut self, frame: &mut Frame, area: Rect, state: &AppState) { // Get the last element in the `visit_stack` vec - let theme = get_theme(); let curr = self .inner_tree .get(*self.visit_stack.last().unwrap()) @@ -141,9 +145,10 @@ impl CustomList { // If we are not at the root of our filesystem tree, we need to add `..` path, to be able // to go up the tree - // icons:   if !self.at_root() { - items.push(Line::from(format!("{} ..", theme.dir_icon)).style(theme.dir_color)); + items.push( + Line::from(format!("{} ..", state.theme.dir_icon)).style(state.theme.dir_color), + ); } // Iterate through all the children @@ -152,13 +157,13 @@ impl CustomList { // it's a directory and will be handled as such if node.has_children() { items.push( - Line::from(format!("{} {}", theme.dir_icon, node.value().name)) - .style(theme.dir_color), + Line::from(format!("{} {}", state.theme.dir_icon, node.value().name)) + .style(state.theme.dir_color), ); } else { items.push( - Line::from(format!("{} {}", theme.cmd_icon, node.value().name)) - .style(theme.cmd_color), + Line::from(format!("{} {}", state.theme.cmd_icon, node.value().name)) + .style(state.theme.cmd_color), ); } } @@ -205,7 +210,7 @@ impl CustomList { } /// Handle key events, we are only interested in `Press` and `Repeat` events - pub fn handle_key(&mut self, event: KeyEvent) -> Option<&'static str> { + pub fn handle_key(&mut self, event: KeyEvent, state: &AppState) -> Option { if event.kind == KeyEventKind::Release { return None; } @@ -235,14 +240,14 @@ impl CustomList { } // The 'p' key toggles the preview on and off KeyCode::Char('p') => { - self.toggle_preview_window(); + self.toggle_preview_window(state); None } KeyCode::Enter => self.handle_enter(), _ => None, } } - fn toggle_preview_window(&mut self) { + fn toggle_preview_window(&mut self, state: &AppState) { // If the preview window is active, disable it if self.preview_window_state.is_some() { self.preview_window_state = None; @@ -251,17 +256,23 @@ impl CustomList { // Get the selected command if let Some(selected_command) = self.get_selected_command() { - // If command is a folder, we don't display a preview - if selected_command.is_empty() { - return; - } - - // Reconstruct the line breaks and file formatting after the - // 'include_str!()' call in the node - let lines: Vec = selected_command - .lines() - .map(|line| line.to_string()) - .collect(); + let lines = match selected_command { + Command::Raw(cmd) => { + // Reconstruct the line breaks and file formatting after the + // 'include_str!()' call in the node + cmd.lines().map(|line| line.to_string()).collect() + } + Command::LocalFile(file_path) => { + let mut full_path = state.temp_path.clone(); + full_path.push(file_path); + let file_contents = std::fs::read_to_string(&full_path) + .map_err(|_| format!("File not found: {:?}", &full_path)) + .unwrap(); + file_contents.lines().map(|line| line.to_string()).collect() + } + // If command is a folder, we don't display a preview + Command::None => return, + }; // Show the preview window with the text lines self.preview_window_state = Some(PreviewWindowState::new(lines)); @@ -315,7 +326,7 @@ impl CustomList { /// /// This could probably be integrated into the 'handle_enter()' method as to avoid code /// duplication, but I don't want to make too major changes to the codebase. - fn get_selected_command(&self) -> Option<&'static str> { + fn get_selected_command(&self) -> Option { let curr = self .inner_tree .get(*self.visit_stack.last().unwrap()) @@ -332,7 +343,7 @@ impl CustomList { idx += 1; } if idx == selected { - return Some(node.value().command); + return Some(node.value().command.clone()); } } None @@ -342,8 +353,9 @@ impl CustomList { /// - Run a command, if it is the currently selected item, /// - Go up a directory /// - Go down into a directory + /// /// Returns `Some(command)` when command is selected, othervise we returns `None` - fn handle_enter(&mut self) -> Option<&'static str> { + fn handle_enter(&mut self) -> Option { // Get the current node (current directory) let curr = self .inner_tree @@ -371,7 +383,7 @@ impl CustomList { self.list_state.select(Some(0)); return None; } else { - return Some(node.value().command); + return Some(node.value().command.clone()); } } } diff --git a/src/main.rs b/src/main.rs index 7acfcfb0..0295336f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod float; mod list; mod running_command; +pub mod state; mod theme; use std::{ @@ -16,13 +17,16 @@ use crossterm::{ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, ExecutableCommand, }; +use include_dir::include_dir; use list::CustomList; use ratatui::{ backend::{Backend, CrosstermBackend}, Terminal, }; use running_command::RunningCommand; -use theme::set_theme; +use state::AppState; +use tempdir::TempDir; +use theme::THEMES; /// This is a binary :), Chris, change this to update the documentation on -h #[derive(Debug, Parser)] @@ -34,16 +38,29 @@ struct Args { fn main() -> std::io::Result<()> { let args = Args::parse(); - if args.compat { - set_theme(0); - } + + let theme = if args.compat { + THEMES[0].clone() + } else { + THEMES[1].clone() + }; + let commands_dir = include_dir!("src/commands"); + let temp_dir: TempDir = TempDir::new("linutil_scripts").unwrap(); + commands_dir + .extract(temp_dir.path()) + .expect("Failed to extract the saved directory"); + + let state = AppState { + theme, + temp_path: temp_dir.path().to_owned(), + }; stdout().execute(EnterAlternateScreen)?; enable_raw_mode()?; let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; terminal.clear()?; - run(&mut terminal)?; + run(&mut terminal, &state)?; // restore terminal disable_raw_mode()?; @@ -55,7 +72,7 @@ fn main() -> std::io::Result<()> { Ok(()) } -fn run(terminal: &mut Terminal) -> io::Result<()> { +fn run(terminal: &mut Terminal, state: &AppState) -> io::Result<()> { let mut command_opt: Option = None; let mut custom_list = CustomList::new(); @@ -63,9 +80,9 @@ fn run(terminal: &mut Terminal) -> io::Result<()> { // Always redraw terminal .draw(|frame| { - custom_list.draw(frame, frame.size()); + custom_list.draw(frame, frame.size(), state); if let Some(ref mut command) = &mut command_opt { - command.draw(frame); + command.draw(frame, state); } }) .unwrap(); @@ -90,8 +107,8 @@ fn run(terminal: &mut Terminal) -> io::Result<()> { if key.code == KeyCode::Char('q') { return Ok(()); } - if let Some(cmd) = custom_list.handle_key(key) { - command_opt = Some(RunningCommand::new(cmd)); + if let Some(cmd) = custom_list.handle_key(key, state) { + command_opt = Some(RunningCommand::new(cmd, state)); } } } diff --git a/src/running_command.rs b/src/running_command.rs index 9d159ef7..ad3196ae 100644 --- a/src/running_command.rs +++ b/src/running_command.rs @@ -11,7 +11,7 @@ use portable_pty::{ }; use ratatui::{ layout::Size, - style::{Color, Style, Stylize}, + style::{Style, Stylize}, text::{Line, Span}, widgets::{Block, Borders}, Frame, @@ -21,7 +21,14 @@ use tui_term::{ widget::PseudoTerminal, }; -use crate::{float::floating_window, theme::get_theme}; +use crate::{float::floating_window, state::AppState}; + +#[derive(Clone)] +pub enum Command { + Raw(&'static str), + LocalFile(&'static str), + None, // Directory +} /// This is a struct for storing everything connected to a running command // Create a new instance on every new command you want to run @@ -50,14 +57,22 @@ pub struct RunningCommand { } impl RunningCommand { - pub fn new(command: &str) -> Self { + pub fn new(command: Command, state: &AppState) -> Self { let pty_system = NativePtySystem::default(); - let mut cmd = CommandBuilder::new("sh"); - cmd.arg("-c"); - cmd.arg(command); - let cwd = std::env::current_dir().unwrap(); - cmd.cwd(cwd); + let mut cmd = CommandBuilder::new("sh"); + match command { + Command::Raw(prompt) => { + cmd.arg("-c"); + cmd.arg(prompt); + } + Command::LocalFile(file) => { + cmd.arg(file); + } + Command::None => panic!("Command::None was treated as a command"), + } + + cmd.cwd(&state.temp_path); let pair = pty_system .openpty(PtySize { @@ -153,59 +168,57 @@ impl RunningCommand { } } - pub fn draw(&mut self, frame: &mut Frame) { - { - let theme = get_theme(); - // Funny name - let floater = floating_window(frame.size()); + pub fn draw(&mut self, frame: &mut Frame, state: &AppState) { + // Funny name + let floater = floating_window(frame.size()); - let inner_size = Size { - width: floater.width - 2, // Because we add a `Block` with a border - height: floater.height - 2, - }; + let inner_size = Size { + width: floater.width - 2, // Because we add a `Block` with a border + height: floater.height - 2, + }; - // When the command is running - let term_border = if !self.is_finished() { - Block::default() - .borders(Borders::ALL) - .title_top(Line::from("Running the command....").centered()) - .title_style(Style::default().reversed()) - .title_bottom(Line::from("Press Ctrl-C to KILL the command")) - } else { - // This portion is just for pretty colors. - // You can use multiple `Span`s with different styles each, to construct a line, - // which can be used as a list item, or in this case a `Block` title + // When the command is running + let term_border = if !self.is_finished() { + Block::default() + .borders(Borders::ALL) + .title_top(Line::from("Running the command....").centered()) + .title_style(Style::default().reversed()) + .title_bottom(Line::from("Press Ctrl-C to KILL the command")) + } else { + // This portion is just for pretty colors. + // You can use multiple `Span`s with different styles each, to construct a line, + // which can be used as a list item, or in this case a `Block` title - let mut title_line = if self.get_exit_status().success() { - Line::from( - Span::default() - .content("SUCCESS!") - .style(Style::default().fg(theme.success_color).reversed()), - ) - } else { - Line::from( - Span::default() - .content("FAILED!") - .style(Style::default().fg(theme.fail_color).reversed()), - ) - }; - - title_line.push_span( + let mut title_line = if self.get_exit_status().success() { + Line::from( Span::default() - .content(" press to close this window ") - .style(Style::default()), - ); - - Block::default() - .borders(Borders::ALL) - .title_top(title_line.centered()) + .content("SUCCESS!") + .style(Style::default().fg(state.theme.success_color).reversed()), + ) + } else { + Line::from( + Span::default() + .content("FAILED!") + .style(Style::default().fg(state.theme.fail_color).reversed()), + ) }; - let screen = self.screen(inner_size); // when the terminal is changing a lot, there - // will be 1 frame of lag on resizing - let pseudo_term = PseudoTerminal::new(&screen).block(term_border); - frame.render_widget(pseudo_term, floater); - } + + title_line.push_span( + Span::default() + .content(" press to close this window ") + .style(Style::default()), + ); + + Block::default() + .borders(Borders::ALL) + .title_top(title_line.centered()) + }; + let screen = self.screen(inner_size); // when the terminal is changing a lot, there + // will be 1 frame of lag on resizing + let pseudo_term = PseudoTerminal::new(&screen).block(term_border); + frame.render_widget(pseudo_term, floater); } + /// Send SIGHUB signal, *not* SIGKILL or SIGTERM, to the child process pub fn kill_child(&mut self) { if !self.is_finished() { diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 00000000..459e7d30 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,9 @@ +use crate::theme::Theme; +use std::path::PathBuf; + +pub struct AppState { + /// Selected theme + pub theme: Theme, + /// Path to the root of the unpacked files in /tmp + pub temp_path: PathBuf, +} diff --git a/src/theme.rs b/src/theme.rs index 71413752..4792cddc 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,7 +1,6 @@ use ratatui::style::Color; -pub static mut THEME_IDX: usize = 1; - +#[derive(Clone)] pub struct Theme { pub dir_color: Color, pub cmd_color: Color, @@ -29,11 +28,3 @@ pub const THEMES: [Theme; 2] = [ success_color: Color::Rgb(5, 255, 55), }, ]; - -pub fn get_theme() -> &'static Theme { - &THEMES[unsafe { THEME_IDX }] -} - -pub fn set_theme(idx: usize) { - unsafe { THEME_IDX = idx }; -}