├── quickrice ├── __init__.py ├── desktop │ ├── __init__.py │ ├── extensions │ │ ├── __init__.py │ │ ├── check_system.py │ │ └── download_extensions.py │ ├── quick_themes │ │ ├── __init__.py │ │ └── theme.py │ ├── detect_desktop.py │ └── desktops.py ├── interactive │ ├── __init__.py │ ├── apply │ │ ├── __init__.py │ │ └── apply_theme.py │ ├── create │ │ ├── __init__.py │ │ ├── desktop_options │ │ │ ├── __init__.py │ │ │ ├── xfce_theme.py │ │ │ ├── cinnamon_theme.py │ │ │ └── gnome_theme.py │ │ ├── get_files.py │ │ └── create_desktop.py │ ├── delete │ │ ├── __init__.py │ │ └── delete_rice.py │ ├── download │ │ ├── __init__.py │ │ └── download_themes.py │ ├── display_list.py │ └── main_menu.py └── main.py ├── setup.py ├── LICENSE ├── .gitignore ├── README.md └── install.sh /quickrice/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quickrice/desktop/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quickrice/interactive/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quickrice/desktop/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quickrice/desktop/quick_themes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quickrice/interactive/apply/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quickrice/interactive/create/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quickrice/interactive/delete/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quickrice/interactive/download/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quickrice/interactive/create/desktop_options/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quickrice/desktop/detect_desktop.py: -------------------------------------------------------------------------------- 1 | import os 2 | from desktop.desktops import GnomeTheme, CinnamonTheme 3 | 4 | def return_desktop(): 5 | 6 | current_desktop = os.getenv('XDG_SESSION_DESKTOP') 7 | 8 | if current_desktop: 9 | return current_desktop.lower() 10 | 11 | return 'Could not find current Desktop. Perhaps you are not on Linux?' 12 | 13 | def detect_desktop_class(): 14 | desktop = return_desktop() 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /quickrice/interactive/download/download_themes.py: -------------------------------------------------------------------------------- 1 | # from desktop.quick_themes.theme import Theme 2 | 3 | # def test_download(): 4 | # Numix_Theme = Theme('Numix', 'https://github.com/i-mint/bluesky/archive/refs/heads/master.zip', 5 | # 'https://github.com/numixproject/numix-icon-theme-circle/archive/refs/heads/master.zip', 6 | # 'https://github.com/numixproject/numix-cursor-theme/releases/download/v1.2/numix-cursor-1.2.tar', 7 | # 'https://github.com/xyperia/Fluent-Dark/archive/refs/heads/main.zip') 8 | 9 | # Numix_Theme.download_and_install() -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import pathlib 3 | 4 | 5 | HERE = pathlib.Path(__file__).parent 6 | 7 | 8 | long_description = (HERE / "README.md").read_text(encoding="utf-8") 9 | 10 | setup( 11 | name="quickrice", 12 | version="1.0.3", 13 | description="A simple CLI tool to manage your desktop themes.", 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | url="https://github.com/halsschmerzen/quickrice", 17 | author="halsschmerzen", 18 | author_email="bastiansteampl@gmail.com", 19 | license="MIT", 20 | classifiers=[ 21 | "License :: OSI Approved :: MIT License", 22 | "Programming Language :: Python :: 3", 23 | "Operating System :: OS Independent", 24 | ], 25 | packages=find_packages(), 26 | python_requires=">=3.6", 27 | entry_points={ 28 | "console_scripts": [ 29 | "quickrice=quickrice.main:main", 30 | ], 31 | }, 32 | include_package_data=True, 33 | zip_safe=False, 34 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2024] [halsschmerzen] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /quickrice/desktop/extensions/check_system.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | def detect_package_manager(): 4 | """Detect the system's package manager.""" 5 | package_managers = { 6 | 'apt': ['sudo', 'apt', 'install', '-y'], 7 | 'dnf': ['sudo', 'dnf', 'install', '-y'], 8 | 'pacman': ['sudo', 'pacman', '-S', '--noconfirm'], 9 | 'zypper': ['sudo', 'zypper', 'install', '-y'] 10 | } 11 | 12 | for manager in package_managers: 13 | if subprocess.run(['which', manager], stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode == 0: 14 | return manager, package_managers[manager] 15 | 16 | return None, "No supported package manager detected on this system." 17 | 18 | def collect_necessary_packages(manager): 19 | """Return the list of necessary packages for the detected package manager.""" 20 | # Define the necessary packages for each package manager 21 | package_map = { 22 | 'apt': ['gnome-tweaks', 'gnome-shell-extensions', 'dconf-cli'], 23 | 'dnf': ['gnome-tweaks', 'gnome-shell-extension-user-theme'], 24 | 'pacman': ['gnome-tweaks', 'gnome-shell-extensions'], 25 | 'zypper': ['gnome-tweaks', 'gnome-shell-extensions'] 26 | } 27 | 28 | if manager not in package_map: 29 | raise ValueError(f"Package manager '{manager}' is not supported.") 30 | 31 | # Return the packages needed for the specific package manager 32 | return package_map[manager] 33 | 34 | -------------------------------------------------------------------------------- /quickrice/interactive/display_list.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | def choose_from_list(options): 4 | if not options: 5 | print("The list is empty.") 6 | return None 7 | 8 | # Determine the number of columns for the grid display 9 | num_columns = math.ceil(math.sqrt(len(options))) # You can adjust this logic if desired 10 | num_rows = math.ceil(len(options) / num_columns) 11 | 12 | print("Please choose an option by entering the corresponding number:") 13 | 14 | # Display the options in a grid format 15 | for row in range(num_rows): 16 | for col in range(num_columns): 17 | index = row * num_columns + col 18 | if index < len(options): 19 | # Print each option in the grid 20 | print(f"{index + 1:2}. {options[index]:<20}", end=' ') # Adjust spacing as necessary 21 | print() # New line after each row 22 | 23 | while True: 24 | try: 25 | # Get user input 26 | choice = int(input("Enter the number of your choice: ")) 27 | 28 | # Validate the choice 29 | if 1 <= choice <= len(options): 30 | selected_option = options[choice - 1] 31 | print(f"You selected: {selected_option}") 32 | return selected_option 33 | else: 34 | print("Invalid choice. Please try again.") 35 | 36 | except ValueError: 37 | print("Please enter a valid number.") -------------------------------------------------------------------------------- /quickrice/desktop/extensions/download_extensions.py: -------------------------------------------------------------------------------- 1 | from desktop.extensions.check_system import detect_package_manager 2 | import subprocess 3 | 4 | def check_package_installed(package, manager): 5 | """Check if a package is installed based on the detected package manager.""" 6 | check_commands = { 7 | 'apt': ['dpkg-query', '-W', '-f=${Status}', package], 8 | 'dnf': ['dnf', 'list', 'installed', package], 9 | 'pacman': ['pacman', '-Q', package], 10 | 'zypper': ['zypper', 'search', '--installed-only', package] 11 | } 12 | 13 | if manager not in check_commands: 14 | raise ValueError(f"Package manager '{manager}' is not supported for checking packages.") 15 | 16 | try: 17 | result = subprocess.run(check_commands[manager], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) 18 | if manager == 'apt': 19 | return "install ok installed" in result.stdout 20 | elif manager == 'pacman': 21 | return result.returncode == 0 22 | else: 23 | return result.returncode == 0 24 | except subprocess.CalledProcessError: 25 | return False 26 | 27 | 28 | def install_using_package_manager(package, manager, install_cmd): 29 | """Install a package using the appropriate package manager.""" 30 | if check_package_installed(package, manager): 31 | print(f"{package} is already installed. No need to reinstall.") 32 | return 33 | 34 | try: 35 | print(f'Installing {package} using {manager}') 36 | subprocess.run(install_cmd + [package], check=True) 37 | print(f'Package {package} installed successfully.') 38 | except subprocess.CalledProcessError as e: 39 | print(f"FAILED! Error installing {package}:\n{e}") 40 | 41 | 42 | def install_necessary_packages(packages): 43 | manager, install_cmd = detect_package_manager() 44 | 45 | if manager is None: 46 | print("No supported package manager found!") 47 | return 48 | 49 | for package in packages: 50 | install_using_package_manager(package, manager, install_cmd) 51 | 52 | print('FINISHED! Installing all packages.') -------------------------------------------------------------------------------- /quickrice/interactive/create/get_files.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | class ThemeOptions: 4 | def __init__(self, theme_dirs): 5 | self.theme_dirs = theme_dirs 6 | 7 | def get_available_themes(self, directories, validation_func=None): 8 | """Fetches available themes from the specified directories and validates them.""" 9 | themes = set() 10 | for directory in directories: 11 | if os.path.exists(directory): 12 | for theme in next(os.walk(directory))[1]: # Get directory names 13 | theme_path = os.path.join(directory, theme) 14 | if validation_func is None or validation_func(theme_path): 15 | themes.add(theme) 16 | return list(themes) 17 | 18 | def choose_from_list(self, theme_list, theme_type): 19 | """Displays a sorted list of themes in grid format and allows the user to choose one by number.""" 20 | if not theme_list: 21 | print(f"No {theme_type} themes available.") 22 | return None 23 | 24 | # Sort the list alphabetically 25 | theme_list = sorted(theme_list) 26 | print(f"\nAvailable {theme_type} themes:") 27 | 28 | # Define the number of columns 29 | num_columns = 3 30 | num_items = len(theme_list) 31 | num_rows = (num_items + num_columns - 1) // num_columns # Ceiling division 32 | 33 | # Create rows for the grid 34 | grid = [] 35 | for row_idx in range(num_rows): 36 | row = [] 37 | for col_idx in range(num_columns): 38 | index = row_idx + num_rows * col_idx 39 | if index < num_items: 40 | theme = theme_list[index] 41 | idx = index + 1 42 | row.append(f"{idx}: {theme}") 43 | else: 44 | row.append('') 45 | grid.append(row) 46 | 47 | # Calculate column width for alignment 48 | col_width = max(len(item) for row in grid for item in row if item) + 2 49 | 50 | # Print the grid 51 | for row in grid: 52 | print("".join(item.ljust(col_width) for item in row)) 53 | 54 | while True: 55 | try: 56 | choice = int(input(f"\nEnter the number of the {theme_type} theme you want to select: ")) 57 | if 1 <= choice <= len(theme_list): 58 | return theme_list[choice - 1] 59 | else: 60 | print(f"Please choose a number between 1 and {len(theme_list)}.") 61 | except ValueError: 62 | print("Invalid input. Please enter a number.") 63 | 64 | -------------------------------------------------------------------------------- /quickrice/interactive/create/desktop_options/xfce_theme.py: -------------------------------------------------------------------------------- 1 | import os 2 | from interactive.create.get_files import ThemeOptions 3 | 4 | class XfceThemeOptions(ThemeOptions): 5 | def __init__(self): 6 | gtk_theme_dirs = ['/usr/share/themes', os.path.expanduser('~/.themes')] 7 | icon_theme_dirs = ['/usr/share/icons', os.path.expanduser('~/.icons')] 8 | super().__init__(gtk_theme_dirs + icon_theme_dirs) 9 | self.gtk_theme_dirs = gtk_theme_dirs 10 | self.icon_theme_dirs = icon_theme_dirs 11 | 12 | @staticmethod 13 | def is_valid_gtk_theme(theme_path): 14 | """Checks if the theme contains valid GTK theme files.""" 15 | return any( 16 | os.path.exists(os.path.join(theme_path, f"gtk-{version}/gtk.css")) 17 | for version in ['3.0', '4.0'] 18 | ) 19 | 20 | @staticmethod 21 | def is_valid_icon_theme(theme_path): 22 | """Checks if the theme contains a valid icon theme file.""" 23 | return os.path.exists(os.path.join(theme_path, "index.theme")) 24 | 25 | @staticmethod 26 | def is_valid_xfwm4_theme(theme_path): 27 | """Checks if the theme contains valid XFWM4 theme files.""" 28 | xfwm4_theme_path = os.path.join(theme_path, 'xfwm4') 29 | return os.path.exists(xfwm4_theme_path) and os.path.exists(os.path.join(xfwm4_theme_path, 'themerc')) 30 | 31 | @staticmethod 32 | def is_valid_cursor_theme(theme_path): 33 | """Checks if the theme contains a valid cursor theme.""" 34 | cursor_path = os.path.join(theme_path, 'cursors') 35 | return os.path.exists(cursor_path) and any(os.path.isfile(os.path.join(cursor_path, file)) for file in ['arrow', 'default']) 36 | 37 | # Methods for fetching themes 38 | def get_available_gtk_themes(self): 39 | """Fetches available GTK themes from system and user directories, validating them.""" 40 | return self.get_available_themes(self.gtk_theme_dirs, validation_func=self.is_valid_gtk_theme) 41 | 42 | def get_available_icon_themes(self): 43 | """Fetches available icon themes from system and user directories, validating them.""" 44 | return self.get_available_themes(self.icon_theme_dirs, validation_func=self.is_valid_icon_theme) 45 | 46 | def get_available_xfwm4_themes(self): 47 | """Fetches available XFWM4 themes, validating them.""" 48 | return self.get_available_themes(self.gtk_theme_dirs, validation_func=self.is_valid_xfwm4_theme) 49 | 50 | def get_available_cursor_themes(self): 51 | """Fetches available cursor themes, validating them.""" 52 | return self.get_available_themes(self.icon_theme_dirs, validation_func=self.is_valid_cursor_theme) -------------------------------------------------------------------------------- /quickrice/interactive/create/desktop_options/cinnamon_theme.py: -------------------------------------------------------------------------------- 1 | import os 2 | from interactive.create.get_files import ThemeOptions 3 | 4 | class CinnamonThemeOptions(ThemeOptions): 5 | def __init__(self): 6 | gtk_theme_dirs = ['/usr/share/themes', os.path.expanduser('~/.themes')] 7 | icon_theme_dirs = ['/usr/share/icons', os.path.expanduser('~/.icons')] 8 | super().__init__(gtk_theme_dirs + icon_theme_dirs) 9 | self.gtk_theme_dirs = gtk_theme_dirs 10 | self.icon_theme_dirs = icon_theme_dirs 11 | 12 | @staticmethod 13 | def is_valid_gtk_theme(theme_path): 14 | """Checks if the theme contains valid GTK theme files.""" 15 | return any( 16 | os.path.exists(os.path.join(theme_path, f"gtk-{version}/gtk.css")) 17 | for version in ['3.0', '4.0'] 18 | ) 19 | 20 | @staticmethod 21 | def is_valid_icon_theme(theme_path): 22 | """Checks if the theme contains a valid icon theme file.""" 23 | return os.path.exists(os.path.join(theme_path, "index.theme")) 24 | 25 | @staticmethod 26 | def is_valid_shell_theme(theme_path): 27 | """Checks if the theme contains valid Cinnamon Shell theme files.""" 28 | shell_theme_path = os.path.join(theme_path, 'cinnamon') 29 | return os.path.exists(shell_theme_path) and os.path.exists(os.path.join(shell_theme_path, 'cinnamon.css')) 30 | 31 | @staticmethod 32 | def is_valid_cursor_theme(theme_path): 33 | """Checks if the theme contains a valid cursor theme.""" 34 | cursor_path = os.path.join(theme_path, 'cursors') 35 | return os.path.exists(cursor_path) and any(os.path.isfile(os.path.join(cursor_path, file)) for file in ['arrow', 'default']) 36 | 37 | # Methods for fetching themes 38 | def get_available_gtk_themes(self): 39 | """Fetches available GTK themes from system and user directories, validating them.""" 40 | return self.get_available_themes(self.gtk_theme_dirs, validation_func=self.is_valid_gtk_theme) 41 | 42 | def get_available_icon_themes(self): 43 | """Fetches available icon themes from system and user directories, validating them.""" 44 | return self.get_available_themes(self.icon_theme_dirs, validation_func=self.is_valid_icon_theme) 45 | 46 | def get_available_shell_themes(self): 47 | """Fetches available Cinnamon Shell themes, validating them.""" 48 | return self.get_available_themes(self.gtk_theme_dirs, validation_func=self.is_valid_shell_theme) 49 | 50 | def get_available_cursor_themes(self): 51 | """Fetches available cursor themes, validating them.""" 52 | return self.get_available_themes(self.icon_theme_dirs, validation_func=self.is_valid_cursor_theme) -------------------------------------------------------------------------------- /quickrice/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import re 5 | from interactive.main_menu import display_main 6 | from interactive.create.create_desktop import create_new_rice 7 | from interactive.apply.apply_theme import apply_theme_by_name, list_available_themes 8 | from desktop.detect_desktop import return_desktop 9 | from desktop.extensions.check_system import detect_package_manager, collect_necessary_packages 10 | from desktop.extensions.download_extensions import check_package_installed, install_necessary_packages 11 | from interactive.delete.delete_rice import delete_theme_by_name 12 | 13 | def main(): 14 | # Parse command-line arguments 15 | parser = argparse.ArgumentParser(description='quickrice - A simple CLI tool to manage your desktop themes.') 16 | subparsers = parser.add_subparsers(dest='command', help='Available commands') 17 | 18 | # 'generate' command 19 | parser_generate = subparsers.add_parser('generate', help='Generate a new rice (theme)') 20 | 21 | # 'apply' command 22 | parser_apply = subparsers.add_parser('apply', help='Apply an existing rice (theme)') 23 | parser_apply.add_argument('theme', nargs='+', help='Name of the theme to apply') 24 | 25 | # 'list' command 26 | parser_list = subparsers.add_parser('list', help='List available themes') 27 | 28 | # 'delete' command 29 | parser_delete = subparsers.add_parser('delete', help='Delete an existing rice (theme)') 30 | parser_delete.add_argument('theme', nargs='+', help='Name of the theme to delete') 31 | 32 | args = parser.parse_args() 33 | 34 | # Check for necessary packages 35 | package_manager, install_command = detect_package_manager() 36 | necessary_packages = collect_necessary_packages(package_manager) 37 | packages_to_install = [] 38 | 39 | for pkg in necessary_packages: 40 | if not check_package_installed(pkg, package_manager): 41 | packages_to_install.append(pkg) 42 | 43 | if packages_to_install: 44 | print(f"Installing necessary packages: {packages_to_install}") 45 | install_necessary_packages(packages_to_install) 46 | else: 47 | print("All necessary packages are already installed.") 48 | 49 | # Execute the command based on user input 50 | if args.command == 'generate': 51 | create_new_rice() 52 | elif args.command == 'apply': 53 | if args.theme: 54 | theme_name = ' '.join(args.theme) 55 | apply_theme_by_name(theme_name) 56 | else: 57 | print('Please specify a theme name to apply.') 58 | parser_apply.print_help() 59 | elif args.command == 'list': 60 | themes = list_available_themes() 61 | if themes: 62 | print('Available Themes:') 63 | for idx, theme in enumerate(themes, start=1): 64 | print(f'{idx}. {theme}') 65 | else: 66 | print('No themes found.') 67 | elif args.command == 'delete': 68 | if args.theme: 69 | theme_name = ' '.join(args.theme) 70 | delete_theme_by_name(theme_name) 71 | else: 72 | print('Please specify a theme name to delete.') 73 | parser_delete.print_help() 74 | else: 75 | display_main() 76 | 77 | if __name__ == '__main__': 78 | main() -------------------------------------------------------------------------------- /quickrice/interactive/main_menu.py: -------------------------------------------------------------------------------- 1 | from desktop.detect_desktop import return_desktop 2 | from interactive.create.create_desktop import create_new_rice, read_theme 3 | from interactive.apply.apply_theme import list_available_themes, choose_theme, create_config_directory 4 | from interactive.delete.delete_rice import delete_rice 5 | from desktop.extensions.check_system import detect_package_manager, collect_necessary_packages 6 | from desktop.extensions.download_extensions import check_package_installed, install_necessary_packages 7 | import re 8 | 9 | package_manager, install_command = detect_package_manager() 10 | necessary_packages = collect_necessary_packages(package_manager) 11 | packages_needed = False 12 | 13 | def banner(): 14 | return r''' 15 | 16 | _ __ 17 | ___ ___ __(_)___/ /__ 18 | / _ `/ // / / __/ '_/ 19 | \_, /\_,_/_/\__/_/\_\ 20 | /_/ ____(_)______ 21 | / __/ / __/ -_) 22 | /_/ /_/\__/\__/ 23 | 24 | quickrice! A simple and quick way to 25 | change your rice from the CLI. 26 | ''' 27 | 28 | def display_main(): 29 | global package_manager, necessary_packages 30 | print(banner()) 31 | create_config_directory() 32 | 33 | # Check for installed packages and collect the ones that are not installed 34 | packages_to_install = [] 35 | 36 | for pkg in necessary_packages: # necessary_packages should be defined beforehand 37 | if not check_package_installed(pkg, package_manager): 38 | packages_to_install.append(pkg) 39 | 40 | if packages_to_install: 41 | print(f"Installing necessary packages: {packages_to_install}") 42 | install_necessary_packages(packages_to_install) 43 | else: 44 | print("All necessary packages are already installed.") 45 | 46 | # Mapping of desktop patterns to theme application functions 47 | # Yes this is stupid and I don't know what I was thinking when writing this 48 | # But for now it works and I will keep it this way :P 49 | desktop_theme_functions = { 50 | r'gnome.*|ubuntu': choose_theme, 51 | r'cinnamon': choose_theme, 52 | r'xfce': choose_theme, 53 | # Add other desktop patterns and their corresponding functions here 54 | } 55 | 56 | while True: 57 | desktop = return_desktop().lower() 58 | print('====================================================') 59 | print(f'You are currently on {desktop}. ') 60 | print('====================================================') 61 | print('Choose your option:') 62 | print('1 - Generate new rice') 63 | print('2 - Apply existing rice') 64 | print('3 - Download theme packages - NOT FULLY IMPLEMENTED') 65 | print('4 - Delete existing rice') 66 | print('5 - Exit') 67 | print('====================================================') 68 | option = int(input('Enter your choice: ')) 69 | 70 | if option == 1: 71 | create_new_rice() 72 | elif option == 2: 73 | for pattern, func in desktop_theme_functions.items(): 74 | if re.match(pattern, desktop): 75 | func() 76 | break 77 | else: 78 | print('Desktop not supported yet!') 79 | elif option == 3: 80 | print('Not fully implemented yet!') 81 | elif option == 4: 82 | delete_rice() 83 | elif option == 5: 84 | print('Exiting quickrice...') 85 | break 86 | else: 87 | print('Invalid option. Please try again.') -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ -------------------------------------------------------------------------------- /quickrice/interactive/create/desktop_options/gnome_theme.py: -------------------------------------------------------------------------------- 1 | import os 2 | from interactive.create.get_files import ThemeOptions 3 | 4 | class GnomeThemeOptions(ThemeOptions): 5 | def __init__(self): 6 | gtk_theme_dirs = ['/usr/share/themes', os.path.expanduser('~/.themes')] 7 | icon_theme_dirs = ['/usr/share/icons', os.path.expanduser('~/.icons')] 8 | super().__init__(gtk_theme_dirs + icon_theme_dirs) 9 | self.gtk_theme_dirs = gtk_theme_dirs 10 | self.icon_theme_dirs = icon_theme_dirs 11 | 12 | 13 | @staticmethod 14 | def is_valid_gtk_theme(theme_path): 15 | """Checks if the theme contains valid GTK theme files.""" 16 | return any( 17 | os.path.exists(os.path.join(theme_path, f"gtk-{version}/gtk.css")) 18 | for version in ['3.0', '4.0'] 19 | ) 20 | 21 | 22 | def check_if_shell_extension(self, extension_id='user-theme@gnome-shell-extensions.gcampax.github.com'): 23 | # Directories to check 24 | user_dir = os.path.expanduser('~/.local/share/gnome-shell/extensions/') 25 | system_dir = '/usr/share/gnome-shell/extensions/' 26 | 27 | # Full paths for user-specific and system-wide extensions 28 | user_extension_path = os.path.join(user_dir, extension_id) 29 | system_extension_path = os.path.join(system_dir, extension_id) 30 | 31 | # Check if extension exists in user-specific or system-wide directories 32 | if os.path.exists(user_extension_path): 33 | return f"Extension '{extension_id}' is installed in the user directory." 34 | elif os.path.exists(system_extension_path): 35 | return f"Extension '{extension_id}' is installed system-wide." 36 | else: 37 | message = f'''User Theme Extension has not been found! Install it now from: 38 | 39 | https://extensions.gnome.org/extension/19/user-themes/ 40 | 41 | Otherwise, the Shell theme will not be present. 42 | 43 | Perhaps one day I can install it automatically from here... 44 | ''' 45 | return message 46 | 47 | 48 | 49 | @staticmethod 50 | def is_valid_icon_theme(theme_path): 51 | """Checks if the theme contains a valid icon theme file.""" 52 | return os.path.exists(os.path.join(theme_path, "index.theme")) 53 | 54 | @staticmethod 55 | def is_valid_shell_theme(theme_path): 56 | """Checks if the theme contains valid GNOME Shell theme files.""" 57 | shell_theme_path = os.path.join(theme_path, 'gnome-shell') 58 | return os.path.exists(shell_theme_path) and os.path.exists(os.path.join(shell_theme_path, 'gnome-shell.css')) 59 | 60 | @staticmethod 61 | def is_valid_cursor_theme(theme_path): 62 | """Checks if the theme contains a valid cursor theme.""" 63 | cursor_path = os.path.join(theme_path, 'cursors') 64 | return os.path.exists(cursor_path) and any(os.path.isfile(os.path.join(cursor_path, file)) for file in ['arrow', 'default']) 65 | 66 | 67 | 68 | # Methods for fetching themes 69 | def get_available_gtk_themes(self): 70 | """Fetches available GTK themes from system and user directories, validating them.""" 71 | return self.get_available_themes(self.gtk_theme_dirs, validation_func=self.is_valid_gtk_theme) 72 | 73 | def get_available_icon_themes(self): 74 | """Fetches available icon themes from system and user directories, validating them.""" 75 | return self.get_available_themes(self.icon_theme_dirs, validation_func=self.is_valid_icon_theme) 76 | 77 | def get_available_shell_themes(self): 78 | """Fetches available GNOME Shell themes, validating them.""" 79 | return self.get_available_themes(self.gtk_theme_dirs, validation_func=self.is_valid_shell_theme) 80 | 81 | def get_available_cursor_themes(self): 82 | """Fetches available cursor themes, validating them.""" 83 | return self.get_available_themes(self.icon_theme_dirs, validation_func=self.is_valid_cursor_theme) 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QuickRice 🌾 2 | 3 | **Supported Desktops:** 🖥️ 4 | 5 | - GNOME 6 | - Cinnamon 7 | - XFCE 8 | 9 | **Unsupported Desktops (as of now - will do them at some point):** ❌ 10 | 11 | - KDE 12 | - MATE 13 | - Others (future support for Window Managers is planned! - haven't figured out how to structure this one yet) 14 | 15 | QuickRice is a simple and efficient command-line tool designed to help you manage and customize your desktop themes (rice) effortlessly. Whether you're looking to generate new themes, apply existing ones, or simply list all available options, QuickRice provides an intuitive interface to streamline your desktop customization experience. 16 | 17 | ## Features ✨ 18 | 19 | - **Generate New Themes:** Create personalized desktop themes tailored to your preferences. 20 | - **Apply Existing Themes:** Easily switch between your saved themes with a single command. 21 | - **List Available Themes:** View all the themes you have created and stored. 22 | - **Interactive Menu:** Navigate through options using an easy-to-use interactive interface. 23 | - **Automated Package Management:** Ensures all necessary packages are installed and up-to-date. 24 | 25 | ## Installation 🛠️ 26 | 27 | ### Prerequisites 📋 28 | 29 | - **Python 3.6+**: Ensure you have Python installed. You can download it from the [official website](https://www.python.org/downloads/). 30 | 31 | ### Using `install.sh` ⚙️ 32 | 33 | It is recommended to use the given install.sh file 34 | Installation using the AUR is supported and installs QuickRice globally by default. 35 | 36 | Make sure the installation script is executable: 37 | 38 | ```bash 39 | chmod +x install.sh 40 | ``` 41 | 42 | For a local installation (recommended) use: 43 | 44 | ```bash 45 | ./install.sh install --local 46 | ``` 47 | 48 | For a global installation (needs sudo privilege) use: 49 | 50 | ```bash 51 | sudo ./install.sh install --global 52 | ``` 53 | 54 | ### Using AUR 🖥️ 55 | 56 | You can install QuickRice via the AUR, which installs it globally by default: 57 | 58 | ```bash 59 | yay -S quickrice 60 | ``` 61 | 62 | ## Usage 🚀 63 | 64 | After installation, you can use QuickRice via the terminal. 65 | 66 | ### Command-Line Interface 🖱️ 67 | 68 | - **Generate a New Theme** 69 | 70 | ```bash 71 | quickrice generate 72 | ``` 73 | 74 | Follow the prompts to create a new desktop theme. 75 | 76 | - **Apply an Existing Theme** 77 | 78 | ```bash 79 | quickrice apply 80 | ``` 81 | 82 | Replace `` with the name of the theme you wish to apply. 83 | 84 | - **List Available Themes** 85 | 86 | ```bash 87 | quickrice list 88 | ``` 89 | 90 | Displays all the themes you have created. 91 | 92 | ### Interactive Menu 🎛️ 93 | 94 | Launch the interactive menu by simply running: 95 | 96 | ```bash 97 | quickrice 98 | ``` 99 | 100 | Navigate through the options to generate, apply, or manage your themes with ease. 101 | 102 | ## Contributing 🤝 103 | 104 | Contributions are welcome! If you'd like to contribute to QuickRice, please follow these steps: 105 | 106 | 1. **Fork the Repository** 🍴 107 | 108 | Click the "Fork" button on the top-right corner of the repository page. 109 | 110 | 2. **Clone Your Fork** 📂 111 | 112 | ```bash 113 | git clone https://github.com/halsschmerzen/quickrice.git 114 | ``` 115 | 116 | 3. **Create a New Branch** 🌲 117 | 118 | ```bash 119 | git checkout -b feature/YourFeatureName 120 | ``` 121 | 122 | 4. **Make Your Changes** ✏️ 123 | 124 | 5. **Commit Your Changes** 💾 125 | 126 | ```bash 127 | git commit -m "Add your message here" 128 | ``` 129 | 130 | 6. **Push to Your Fork** 🚀 131 | 132 | ```bash 133 | git push origin feature/YourFeatureName 134 | ``` 135 | 136 | 7. **Open a Pull Request** 📨 137 | 138 | Navigate to the original repository and open a pull request with your changes. 139 | 140 | ## License 📄 141 | 142 | This project is licensed under the MIT License. 143 | 144 | ## Support 🆘 145 | 146 | If you encounter any issues or have questions, feel free to open an [issue](https://github.com/halsschmerzen/quickrice/issues) on GitHub. 147 | 148 | ## Acknowledgements 🙏 149 | 150 | - Inspired by various desktop customization tools. 151 | - Thanks to the open-source community for their invaluable resources and support. 152 | -------------------------------------------------------------------------------- /quickrice/interactive/delete/delete_rice.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import curses 4 | import re 5 | from desktop.detect_desktop import return_desktop 6 | 7 | config_dir = os.path.expanduser('~/.config/quickrice/rices') 8 | current_desktop = return_desktop().lower() 9 | folders = { 10 | "gnome": [r'gnome.*|ubuntu'], 11 | "cinnamon": [r'cinnamon'], 12 | "xfce": [r'xfce'] 13 | } 14 | def list_themes(): 15 | themes = [] 16 | if current_desktop in folders: 17 | for pattern in folders[current_desktop]: 18 | for root, dirs, files in os.walk(config_dir): 19 | if re.search(pattern, root): 20 | for file in files: 21 | if file.endswith('.json'): 22 | themes.append(file) 23 | return themes 24 | 25 | 26 | def delete_theme_by_name(theme_name): 27 | 28 | for desktop, patterns in folders.items(): 29 | for pattern in patterns: 30 | if re.match(pattern, current_desktop): 31 | desktop_dir = os.path.join(config_dir, desktop) 32 | break 33 | 34 | if desktop_dir is None: 35 | print('Invalid theme name!!') 36 | 37 | 38 | theme_file = os.path.join(desktop_dir, theme_name + '.json') 39 | if os.path.exists(theme_file): 40 | os.remove(theme_file) 41 | print(f'Theme {theme_name} deleted succesfully') 42 | else: 43 | print(f'Invalid theme name!') 44 | 45 | 46 | 47 | def delete_themes(selected_themes): 48 | for theme in selected_themes: 49 | theme_path = os.path.join(config_dir, current_desktop, theme) 50 | if os.path.exists(theme_path): 51 | os.remove(theme_path) 52 | print(f"Deleted theme: {theme}") 53 | else: 54 | print(f"Theme not found: {theme}") 55 | 56 | def curses_menu(stdscr, themes): 57 | curses.curs_set(0) 58 | curses.start_color() 59 | curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE) 60 | selected = [False] * len(themes) 61 | current_row = 0 62 | 63 | while True: 64 | stdscr.clear() 65 | h, w = stdscr.getmaxyx() 66 | 67 | for idx, theme in enumerate(themes): 68 | x = w//2 - len(theme)//2 69 | y = h//2 - len(themes)//2 + idx 70 | if idx == current_row: 71 | stdscr.attron(curses.color_pair(1)) 72 | stdscr.addstr(y, x, theme) 73 | stdscr.attroff(curses.color_pair(1)) 74 | stdscr.addstr(y, x + len(theme) + 2, "<--------") 75 | else: 76 | stdscr.addstr(y, x, theme) 77 | 78 | if selected[idx]: 79 | stdscr.addstr(y, x - 6, "[X]") 80 | 81 | stdscr.addstr(h-2,w//2-len("ESC to leave (might take a second, curses is slow)")//2, "ESC to leave (might take a second, curses is slow)") 82 | stdscr.refresh() 83 | 84 | key = stdscr.getch() 85 | 86 | if key == curses.KEY_UP and current_row > 0: 87 | current_row -= 1 88 | elif key == curses.KEY_DOWN and current_row < len(themes) - 1: 89 | current_row += 1 90 | elif key == curses.KEY_ENTER or key in [10, 13]: 91 | selected[current_row] = not selected[current_row] 92 | elif key == 27: # ESC 93 | if any(selected): 94 | stdscr.clear() 95 | stdscr.addstr(h//2, w//2 - 20, "Are you sure you want to delete the selected themens? [y/n]") 96 | stdscr.refresh() 97 | confirm_key = stdscr.getch() 98 | 99 | if confirm_key in [ord('y'), ord('Y')]: 100 | return [theme for idx, theme in enumerate(themes) if selected[idx]] 101 | else: 102 | return[] 103 | else: 104 | return [] 105 | 106 | def delete_rice(): 107 | themes = list_themes() 108 | if not themes: 109 | print("No themes available for deletion.") 110 | return 111 | 112 | selected_themes = curses.wrapper(curses_menu, themes) 113 | 114 | if selected_themes: 115 | delete_themes(selected_themes) 116 | else: 117 | print("No themes selected for deletion.") 118 | -------------------------------------------------------------------------------- /quickrice/interactive/apply/apply_theme.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import re 4 | from desktop.detect_desktop import return_desktop 5 | from interactive.create.create_desktop import read_theme 6 | from interactive.display_list import choose_from_list 7 | from desktop.desktops import GnomeTheme, CinnamonTheme, XfceTheme 8 | 9 | config_dir = os.path.expanduser('~/.config/quickrice/rices') 10 | 11 | def create_config_directory(): 12 | if not os.path.exists(config_dir): 13 | os.makedirs(config_dir) 14 | 15 | def apply_theme_by_name(theme_name): 16 | current_desktop = return_desktop().lower() 17 | 18 | desktop_patterns = { 19 | 'gnome': [r'gnome.*', r'ubuntu'], 20 | 'cinnamon': [r'cinnamon'], 21 | 'xfce': [r'xfce'], 22 | # Add other desktop patterns and their corresponding functions here 23 | } 24 | 25 | desktop_dir = None 26 | apply_theme_func = None 27 | 28 | for desktop, patterns in desktop_patterns.items(): 29 | for pattern in patterns: 30 | if re.match(pattern, current_desktop): 31 | desktop_dir = os.path.join(config_dir, desktop) 32 | apply_theme_func = globals().get(f'apply_{desktop}_theme') 33 | break 34 | if desktop_dir: 35 | break 36 | else: 37 | print('Desktop not supported yet!') 38 | return 39 | 40 | if not desktop_dir or not os.path.exists(desktop_dir): 41 | print('No themes directory found for your desktop environment.') 42 | return 43 | 44 | theme_file = os.path.join(desktop_dir, theme_name + '.json') 45 | if not os.path.exists(theme_file): 46 | print(f'Theme "{theme_name}" not found in {desktop_dir}.') 47 | return 48 | 49 | try: 50 | with open(theme_file, 'r') as json_file: 51 | theme_data = json.load(json_file) 52 | if apply_theme_func: 53 | apply_theme_func(theme_data) 54 | else: 55 | print('No function available to apply theme for your desktop environment.') 56 | except json.JSONDecodeError: 57 | print(f'Error decoding JSON in file: {theme_file}') 58 | 59 | def list_available_themes(): 60 | themes_for_this_desktop = [] 61 | current_desktop = return_desktop().lower() 62 | 63 | desktop_patterns = { 64 | 'gnome': [r'gnome.*', r'ubuntu', r'mint'], 65 | 'cinnamon': [r'cinnamon'], 66 | 'xfce': [r'xfce'], 67 | } 68 | 69 | desktop_dir = None 70 | for desktop, patterns in desktop_patterns.items(): 71 | for pattern in patterns: 72 | if re.match(pattern, current_desktop): 73 | desktop_dir = os.path.join(config_dir, desktop) 74 | break 75 | else: 76 | continue 77 | break 78 | else: 79 | print('Desktop not supported yet!') 80 | return themes_for_this_desktop 81 | 82 | # Check if the directory is empty 83 | if not os.path.exists(desktop_dir) or not os.listdir(desktop_dir): 84 | print('You haven\'t created any themes yet!') 85 | return themes_for_this_desktop 86 | 87 | for filename in os.listdir(desktop_dir): 88 | if filename.endswith('.json'): 89 | path = os.path.join(desktop_dir, filename) 90 | try: 91 | with open(path, 'r') as json_file: 92 | data = json.load(json_file) 93 | if 'desktop' in data: 94 | for pattern in patterns: 95 | if re.match(pattern, data['desktop']): 96 | theme_name = os.path.splitext(filename)[0] 97 | themes_for_this_desktop.append(theme_name) 98 | break 99 | except json.JSONDecodeError: 100 | print(f'Error decoding JSON in file: {filename}') 101 | return themes_for_this_desktop 102 | 103 | def apply_theme(theme_data, theme_class): 104 | selected_gtk_theme = theme_data.get('gtk_theme') 105 | selected_icon_theme = theme_data.get('icon_theme') 106 | selected_shell_theme = theme_data.get('shell_theme') 107 | selected_xfwm4_theme = theme_data.get('xfwm4_theme') 108 | selected_cursor_theme = theme_data.get('cursor_theme') 109 | selected_font = theme_data.get('font') 110 | selected_color = theme_data.get('color_scheme') 111 | selected_background = theme_data.get('background') 112 | 113 | theme = theme_class( 114 | selected_gtk_theme, 115 | selected_icon_theme, 116 | selected_cursor_theme, 117 | selected_font, 118 | selected_color 119 | ) 120 | 121 | # Apply the selected themes using the theme class methods 122 | theme.set_gtk_theme(selected_gtk_theme) 123 | theme.set_icon_theme(selected_icon_theme) 124 | if hasattr(theme, 'set_shell_theme'): 125 | theme.set_shell_theme(selected_shell_theme) 126 | if hasattr(theme, 'set_xfwm4_theme'): 127 | theme.set_xfwm4_theme(selected_xfwm4_theme) 128 | theme.set_cursor_theme(selected_cursor_theme) 129 | theme.set_color_scheme(selected_color) 130 | 131 | if selected_font: 132 | theme.set_font(selected_font) 133 | else: 134 | theme.set_font('Cantarell') 135 | 136 | if selected_background: 137 | theme.set_wallpaper(selected_background, selected_color) 138 | 139 | def choose_theme(): 140 | available_themes = list_available_themes() 141 | if not available_themes: 142 | print('You have not created any themes yet!') 143 | return 144 | 145 | selected_theme_name = choose_from_list(available_themes) 146 | 147 | if selected_theme_name is None: 148 | print('No theme selected.') 149 | return 150 | 151 | apply_theme_by_name(selected_theme_name) 152 | 153 | def apply_gnome_theme(theme_data): 154 | apply_theme(theme_data, GnomeTheme) 155 | 156 | def apply_cinnamon_theme(theme_data): 157 | apply_theme(theme_data, CinnamonTheme) 158 | 159 | def apply_xfce_theme(theme_data): 160 | apply_theme(theme_data, XfceTheme) -------------------------------------------------------------------------------- /quickrice/desktop/desktops.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | class DesktopTheme: 4 | def __init__(self, gtk_theme, icon_theme, cursor_theme, font, color_scheme): 5 | self.gtk_theme = gtk_theme 6 | self.icon_theme = icon_theme 7 | self.cursor_theme = cursor_theme 8 | self.font = font 9 | self.color_scheme = color_scheme 10 | 11 | def apply_theme(self): 12 | """Applies the current theme settings.""" 13 | self.set_gtk_theme(self.gtk_theme) 14 | self.set_icon_theme(self.icon_theme) 15 | self.set_cursor_theme(self.cursor_theme) 16 | self.set_font(self.font) 17 | self.set_color_scheme(self.color_scheme) 18 | print("Theme applied successfully") 19 | 20 | def set_gtk_theme(self, theme_name): 21 | """Sets the GTK theme.""" 22 | print(f"Setting GTK theme to {theme_name}") 23 | raise NotImplementedError("This method should be implemented by a subclass") 24 | 25 | def set_icon_theme(self, theme_name): 26 | """Sets the icon theme.""" 27 | print(f"Setting Icon theme to {theme_name}") 28 | raise NotImplementedError("This method should be implemented by a subclass") 29 | 30 | def set_cursor_theme(self, theme_name): 31 | """Sets the cursor theme.""" 32 | print(f"Setting Cursor theme to {theme_name}") 33 | raise NotImplementedError("This method should be implemented by a subclass") 34 | 35 | def set_font(self, font_name): 36 | """Sets the default UI font.""" 37 | print(f"Setting UI Font to {font_name}") 38 | raise NotImplementedError("This method should be implemented by a subclass") 39 | 40 | def set_color_scheme(self, scheme): 41 | """Sets the color scheme (light or dark).""" 42 | print(f"Setting color scheme to {scheme}") 43 | raise NotImplementedError("This method should be implemented by a subclass") 44 | 45 | def reset_to_default(self): 46 | """Resets theme settings to desktop defaults.""" 47 | print("Resetting to default theme") 48 | raise NotImplementedError("This method should be implemented by a subclass") 49 | 50 | # Cinnamon subclass 51 | class CinnamonTheme(DesktopTheme): 52 | def set_gtk_theme(self, theme_name): 53 | os.system(f"gsettings set org.cinnamon.desktop.interface gtk-theme {theme_name}") 54 | 55 | def set_icon_theme(self, theme_name): 56 | os.system(f"gsettings set org.cinnamon.desktop.interface icon-theme {theme_name}") 57 | 58 | def set_shell_theme(self, theme_name): 59 | os.system(f"gsettings set org.cinnamon.theme name {theme_name}") 60 | 61 | def set_cursor_theme(self, theme_name): 62 | os.system(f"gsettings set org.cinnamon.desktop.interface cursor-theme {theme_name}") 63 | 64 | def set_font(self, font_name): 65 | os.system(f"gsettings set org.cinnamon.desktop.interface font-name '{font_name} 11'") 66 | 67 | def reset_to_default(self): 68 | self.gtk_theme = "Mint-Y" 69 | self.icon_theme = "Mint-Y" 70 | self.cursor_theme = "DMZ-White" 71 | self.font = "Cantarell" 72 | self.apply_theme() 73 | 74 | #XFCE subclass 75 | class XfceTheme(DesktopTheme): 76 | def set_gtk_theme(self, theme_name): 77 | os.system(f"xfconf-query -c xsettings -p /Net/ThemeName -s {theme_name}") 78 | 79 | def set_icon_theme(self, theme_name): 80 | os.system(f"xfconf-query -c xsettings -p /Net/IconThemeName -s {theme_name}") 81 | 82 | def set_cursor_theme(self, theme_name): 83 | os.system(f"xfconf-query -c xsettings -p /Gtk/CursorThemeName -s {theme_name}") 84 | 85 | def set_font(self, font_name): 86 | os.system(f"xfconf-query -c xsettings -p /Gtk/FontName -s '{font_name} 11'") 87 | 88 | def set_xfwm4_theme(self, theme_name): 89 | os.system(f"xfconf-query -c xfwm4 -p /general/theme -s {theme_name}") 90 | 91 | def set_color_scheme(self, scheme): 92 | if scheme == "dark": 93 | os.system("xfconf-query -c xsettings -p /Net/ThemeVariant -s dark") 94 | else: 95 | os.system("xfconf-query -c xsettings -p /Net/ThemeVariant -s light") 96 | 97 | def set_wallpaper(self, wallpaper_path): 98 | os.system(f"xfconf-query -c xfce4-desktop -p /backdrop/screen0/monitor0/workspace0/last-image -s {wallpaper_path}") 99 | 100 | def reset_to_default(self): 101 | self.gtk_theme = "Adwaita" 102 | self.icon_theme = "Adwaita" 103 | self.cursor_theme = "Adwaita" 104 | self.font = "Cantarell" 105 | self.color_scheme = "light" 106 | self.apply_theme() 107 | 108 | # GNOME subclass 109 | class GnomeTheme(DesktopTheme): 110 | def set_gtk_theme(self, theme_name): 111 | os.system(f"gsettings set org.gnome.desktop.interface gtk-theme {theme_name}") 112 | 113 | def set_icon_theme(self, theme_name): 114 | os.system(f"gsettings set org.gnome.desktop.interface icon-theme {theme_name}") 115 | 116 | def set_cursor_theme(self, theme_name): 117 | os.system(f"gsettings set org.gnome.desktop.interface cursor-theme {theme_name}") 118 | 119 | def set_font(self, font_name): 120 | os.system(f"gsettings set org.gnome.desktop.interface font-name '{font_name} 11'") 121 | 122 | def set_color_scheme(self, scheme): 123 | if scheme == "dark": 124 | os.system("gsettings set org.gnome.desktop.interface color-scheme prefer-dark") 125 | else: 126 | os.system("gsettings set org.gnome.desktop.interface color-scheme prefer-light") 127 | 128 | def set_shell_theme(self, theme_name): 129 | os.system(f"gsettings set org.gnome.shell.extensions.user-theme name {theme_name}") 130 | 131 | def set_wallpaper(self, wallpaper_path, scheme): 132 | if scheme == "dark": 133 | os.system(f"gsettings set org.gnome.desktop.background picture-uri-dark 'file://{wallpaper_path}'") 134 | else: 135 | os.system(f"gsettings set org.gnome.desktop.background picture-uri 'file://{wallpaper_path}'") 136 | 137 | def reset_to_default(self): 138 | self.gtk_theme = "Adwaita" 139 | self.icon_theme = "Adwaita" 140 | self.cursor_theme = "Adwaita" 141 | self.font = "Cantarell" 142 | self.color_scheme = "light" 143 | self.apply_theme() 144 | -------------------------------------------------------------------------------- /quickrice/desktop/quick_themes/theme.py: -------------------------------------------------------------------------------- 1 | # import os 2 | # import requests 3 | # import zipfile 4 | # import tarfile 5 | # from pathlib import Path 6 | # import shutil 7 | 8 | # class Theme: 9 | # def __init__(self, name, gtk_link, icon_link, cursor_link, shell_link): 10 | # self.name = name # Name of the complete theme 11 | # self.gtk_link = gtk_link # Download link for GTK theme 12 | # self.icon_link = icon_link # Download link for Icons 13 | # self.cursor_link = cursor_link # Download link for Cursors 14 | # self.shell_link = shell_link # Download link for Shell theme 15 | 16 | # def get_download_links(self): 17 | # """Return a dictionary of all download links.""" 18 | # return { 19 | # 'GTK': self.gtk_link, 20 | # 'Icons': self.icon_link, 21 | # 'Cursors': self.cursor_link, 22 | # 'Shell': self.shell_link 23 | # } 24 | 25 | # def validate_theme(self, extract_path, theme_type): 26 | # """Check if the directory contains valid theme files.""" 27 | # if theme_type == 'GTK': 28 | # return any((extract_path / d).exists() for d in ['gtk-3.0', 'gtk-2.0']) 29 | 30 | # elif theme_type == 'Icons': 31 | # return (extract_path / 'index.theme').exists() 32 | 33 | # elif theme_type == 'Cursors': 34 | # return (extract_path / 'cursors').exists() 35 | 36 | # elif theme_type == 'Shell': 37 | # return (extract_path / 'gnome-shell').exists() 38 | 39 | # return False 40 | 41 | # def handle_existing_theme(self, theme_dir): 42 | # """Prompt user about handling existing theme directories.""" 43 | # if theme_dir.exists(): 44 | # response = input(f"{theme_dir.name} already exists. Do you want to overwrite (O), rename (R), or skip (S)? ").strip().lower() 45 | # if response == 'o': 46 | # # Overwrite the existing theme 47 | # shutil.rmtree(theme_dir) # Remove existing theme directory 48 | # elif response == 'r': 49 | # # Rename new theme directory to avoid conflict 50 | # new_name = input("Enter a new name for the theme: ").strip() 51 | # theme_dir = theme_dir.parent / new_name # Update path with new name 52 | # if theme_dir.exists(): 53 | # print(f"Theme name '{new_name}' already exists. Skipping installation.") 54 | # return None # Skip this installation 55 | # elif response == 's': 56 | # print(f"Skipping installation of {theme_dir.name}.") 57 | # return None # Skip this installation 58 | 59 | # return theme_dir # Return the final path for installation 60 | 61 | # def download_and_install(self): 62 | # """Download and install the theme components.""" 63 | # download_links = self.get_download_links() 64 | 65 | # themes_dir = Path.home() / ".themes" 66 | # icons_dir = Path.home() / ".icons" 67 | # shell_dir = Path.home() / ".local/share/gnome-shell/extensions" 68 | 69 | # themes_dir.mkdir(parents=True, exist_ok=True) 70 | # icons_dir.mkdir(parents=True, exist_ok=True) 71 | # shell_dir.mkdir(parents=True, exist_ok=True) 72 | 73 | # for theme_type, url in download_links.items(): 74 | # print(f"Downloading {theme_type} from {url}...") 75 | # response = requests.get(url) 76 | # response.raise_for_status() # Raise an error for bad responses 77 | 78 | # # Determine the file extension to handle extraction 79 | # filename = url.split("/")[-1] 80 | # filepath = Path.home() / filename 81 | 82 | # # Save the file 83 | # with open(filepath, 'wb') as file: 84 | # file.write(response.content) 85 | 86 | # # Extract files based on the file type 87 | # temp_dir = Path.home() / "temp_extracted" 88 | # temp_dir.mkdir(parents=True, exist_ok=True) 89 | 90 | # try: 91 | # if filename.endswith('.zip'): 92 | # with zipfile.ZipFile(filepath, 'r') as zip_ref: 93 | # zip_ref.extractall(temp_dir) 94 | # elif filename.endswith('.tar.gz'): 95 | # with tarfile.open(filepath, 'r:gz') as tar_ref: 96 | # tar_ref.extractall(temp_dir) 97 | # elif filename.endswith('.tar'): 98 | # with tarfile.open(filepath, 'r:') as tar_ref: 99 | # tar_ref.extractall(temp_dir) 100 | # else: 101 | # print(f"Warning: Unsupported file type for {filename}.") 102 | # continue 103 | # except Exception as e: 104 | # print(f"Error extracting {filename}: {e}") 105 | # continue 106 | 107 | # # Check if the temp_dir contains only one folder 108 | # extracted_folders = list(temp_dir.iterdir()) 109 | # if len(extracted_folders) == 1 and extracted_folders[0].is_dir(): 110 | # # If there's only one folder, get that folder's path 111 | # main_theme_folder = extracted_folders[0] 112 | 113 | # # Move valid themes from the main folder to the appropriate directories 114 | # for extracted_theme in main_theme_folder.iterdir(): 115 | # if extracted_theme.is_dir(): 116 | # # Determine the target directory 117 | # target_dir = themes_dir / extracted_theme.name if self.validate_theme(extracted_theme, 'GTK') else \ 118 | # icons_dir / extracted_theme.name if self.validate_theme(extracted_theme, 'Icons') else \ 119 | # icons_dir / extracted_theme.name if self.validate_theme(extracted_theme, 'Cursors') else \ 120 | # shell_dir / extracted_theme.name if self.validate_theme(extracted_theme, 'Shell') else None 121 | 122 | # if target_dir: 123 | # # Handle existing theme paths 124 | # final_target_dir = self.handle_existing_theme(target_dir) 125 | # if final_target_dir is not None: 126 | # shutil.move(str(extracted_theme), str(final_target_dir)) 127 | # print(f"{theme_type} Theme {extracted_theme.name} installed successfully at {final_target_dir}.") 128 | # else: 129 | # print(f"Warning: {extracted_theme.name} is not a valid theme.") 130 | # else: 131 | # # If there are multiple folders or none, handle them as before 132 | # for extracted_theme in temp_dir.iterdir(): 133 | # if extracted_theme.is_dir(): 134 | # target_dir = themes_dir / extracted_theme.name if self.validate_theme(extracted_theme, 'GTK') else \ 135 | # icons_dir / extracted_theme.name if self.validate_theme(extracted_theme, 'Icons') else \ 136 | # icons_dir / extracted_theme.name if self.validate_theme(extracted_theme, 'Cursors') else \ 137 | # shell_dir / extracted_theme.name if self.validate_theme(extracted_theme, 'Shell') else None 138 | 139 | # if target_dir: 140 | # final_target_dir = self.handle_existing_theme(target_dir) 141 | # if final_target_dir is not None: 142 | # shutil.move(str(extracted_theme), str(final_target_dir)) 143 | # print(f"{theme_type} Theme {extracted_theme.name} installed successfully at {final_target_dir}.") 144 | # else: 145 | # print(f"Warning: {extracted_theme.name} is not a valid theme.") 146 | 147 | # # Clean up 148 | # shutil.rmtree(temp_dir) # Remove temporary extraction directory 149 | # os.remove(filepath) # Remove the downloaded file 150 | 151 | # def __str__(self): 152 | # return f"Theme: {self.name}\n" \ 153 | # f" GTK: {self.gtk_link}\n" \ 154 | # f" Icons: {self.icon_link}\n" \ 155 | # f" Cursors: {self.cursor_link}\n" \ 156 | # f" Shell: {self.shell_link}\n" 157 | -------------------------------------------------------------------------------- /quickrice/interactive/create/create_desktop.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import subprocess 4 | import re 5 | from desktop.detect_desktop import return_desktop 6 | from interactive.create.desktop_options.gnome_theme import GnomeThemeOptions 7 | from interactive.create.desktop_options.cinnamon_theme import CinnamonThemeOptions 8 | from interactive.create.desktop_options.xfce_theme import XfceThemeOptions 9 | 10 | config_dir = os.path.expanduser('~/.config/quickrice/rices') 11 | 12 | def create_new_rice(): 13 | current_desktop = return_desktop().lower() 14 | 15 | available_desktops = { 16 | r'gnome.*|ubuntu' : create_gnome_theme, 17 | r'cinnamon' : create_cinnamon_theme, 18 | r'xfce' : create_xfce_theme 19 | } 20 | 21 | for pattern, method in available_desktops.items(): 22 | if re.match(pattern, current_desktop): 23 | method(current_desktop) 24 | break 25 | else: 26 | print('Desktop not supported yet!') 27 | 28 | def is_user_themes_enabled(): 29 | result = subprocess.run( 30 | ["gnome-extensions", "info", "user-theme@gnome-shell-extensions.gcampax.github.com"], 31 | capture_output=True, 32 | text=True 33 | ) 34 | return "State: ENABLED" in result.stdout or "State: ACTIVE" in result.stdout 35 | def enable_user_themes(): 36 | if is_user_themes_enabled(): 37 | print("User Themes extension is already enabled.") 38 | return 39 | user_input = input("User Themes extension is not enabled. Do you want to enable it? (y/n): ").strip().lower() 40 | 41 | if user_input in ['yes', 'y']: 42 | # Enable the User Themes extension using the gnome-extensions command, hopefully the user installed it previously. 43 | subprocess.run( 44 | ["gnome-extensions", "enable", "user-theme@gnome-shell-extensions.gcampax.github.com"] 45 | ) 46 | print("User Themes extension has been enabled.") 47 | else: 48 | print("User Themes extension was not enabled.") 49 | 50 | def create_gnome_theme(current_desktop): 51 | theme_options = GnomeThemeOptions() 52 | 53 | if not is_user_themes_enabled(): 54 | enable_user_themes() 55 | 56 | print(theme_options.check_if_shell_extension()) 57 | 58 | gtk_themes = theme_options.get_available_gtk_themes() 59 | icon_themes = theme_options.get_available_icon_themes() 60 | shell_themes = theme_options.get_available_shell_themes() 61 | cursor_themes = theme_options.get_available_cursor_themes() 62 | 63 | selected_gtk_theme = theme_options.choose_from_list(gtk_themes, "GTK") 64 | selected_icon_theme = theme_options.choose_from_list(icon_themes, "Icon") 65 | selected_shell_theme = theme_options.choose_from_list(shell_themes, "Shell") 66 | selected_cursor_theme = theme_options.choose_from_list(cursor_themes, "Cursor") 67 | selected_font = None 68 | selected_color_scheme = 'dark' 69 | selected_background = None 70 | 71 | font_input = str(input('Do you want to set a Font? [y/n]')) 72 | 73 | if font_input == 'y': 74 | font_value = str(input('Please enter the name of the font you want to apply: \n')) 75 | selected_font = font_value 76 | 77 | color_input = str(input('Do you want the preferred color scheme to be light? [Default Value: Dark] [y/n]')) 78 | if color_input == 'y': 79 | selected_color_scheme = 'light' 80 | 81 | bg_input = str(input('Do you want to set a matching wallpaper? [y/n]')) 82 | if bg_input == 'y': 83 | background_path = str(input('Enter the path to the desired wallpaper: \n')) 84 | selected_background = background_path 85 | 86 | selections = { 87 | "desktop" : "gnome", 88 | "gtk_theme": selected_gtk_theme, 89 | "icon_theme": selected_icon_theme, 90 | "shell_theme": selected_shell_theme, 91 | "cursor_theme": selected_cursor_theme, 92 | "font": selected_font, 93 | "color_scheme": selected_color_scheme, 94 | "background": selected_background 95 | } 96 | 97 | desktop_dir = os.path.join(config_dir, "gnome") 98 | os.makedirs(desktop_dir, exist_ok=True) 99 | 100 | while True: 101 | theme_name = str(input('Enter the rice name: ')) 102 | 103 | if os.path.exists(f'{desktop_dir}/{theme_name}.json'): 104 | print('Theme with that name already exists!') 105 | else: 106 | write_to_json(selections, theme_name, desktop_dir) 107 | break 108 | 109 | def create_cinnamon_theme(current_desktop): 110 | theme_options = CinnamonThemeOptions() 111 | 112 | gtk_themes = theme_options.get_available_gtk_themes() 113 | icon_themes = theme_options.get_available_icon_themes() 114 | shell_themes = theme_options.get_available_shell_themes() 115 | cursor_themes = theme_options.get_available_cursor_themes() 116 | 117 | selected_gtk_theme = theme_options.choose_from_list(gtk_themes, "GTK") 118 | selected_icon_theme = theme_options.choose_from_list(icon_themes, "Icon") 119 | selected_shell_theme = theme_options.choose_from_list(shell_themes, "Shell") 120 | selected_cursor_theme = theme_options.choose_from_list(cursor_themes, "Cursor") 121 | selected_font = None 122 | 123 | 124 | font_input = str(input('Do you want to set a Font? [y/n]')) 125 | 126 | if font_input == 'y': 127 | font_value = str(input('Please enter the name of the font you want to apply: \n')) 128 | selected_font = font_value 129 | 130 | 131 | selections = { 132 | "desktop" : "cinnamon", 133 | "gtk_theme": selected_gtk_theme, 134 | "icon_theme": selected_icon_theme, 135 | "shell_theme": selected_shell_theme, 136 | "cursor_theme": selected_cursor_theme, 137 | "font": selected_font, 138 | } 139 | 140 | desktop_dir = os.path.join(config_dir, "cinnamon") 141 | os.makedirs(desktop_dir, exist_ok=True) 142 | 143 | while True: 144 | theme_name = str(input('Enter the rice name: ')) 145 | 146 | if os.path.exists(f'{desktop_dir}/{theme_name}.json'): 147 | print('Theme with that name already exists!') 148 | else: 149 | write_to_json(selections, theme_name, desktop_dir) 150 | break 151 | 152 | def create_xfce_theme(current_desktop): 153 | theme_options = XfceThemeOptions() 154 | 155 | gtk_themes = theme_options.get_available_gtk_themes() 156 | icon_themes = theme_options.get_available_icon_themes() 157 | xfwm4_themes = theme_options.get_available_xfwm4_themes() 158 | cursor_themes = theme_options.get_available_cursor_themes() 159 | 160 | selected_gtk_theme = theme_options.choose_from_list(gtk_themes, "GTK") 161 | selected_icon_theme = theme_options.choose_from_list(icon_themes, "Icon") 162 | selected_xfwm4_theme = theme_options.choose_from_list(xfwm4_themes, "XFWM4") 163 | selected_cursor_theme = theme_options.choose_from_list(cursor_themes, "Cursor") 164 | selected_color_scheme = 'dark' 165 | selected_font = None 166 | selected_background = None 167 | 168 | font_input = str(input('Do you want to set a Font? [y/n]')) 169 | 170 | if font_input == 'y': 171 | font_value = str(input('Please enter the name of the font you want to apply: \n')) 172 | selected_font = font_value 173 | 174 | color_input = str(input('Do you want the preferred color scheme to be light? [Default Value: Dark] [y/n]')) 175 | 176 | if color_input == 'y': 177 | selected_color_scheme = 'light' 178 | 179 | bg_input = str(input('Do you want to set a matching wallpaper? [y/n]')) 180 | 181 | if bg_input == 'y': 182 | background_path = str(input('Enter the path to the desired wallpaper: \n')) 183 | selected_background = background_path 184 | 185 | selections = { 186 | "desktop" : "xfce", 187 | "gtk_theme": selected_gtk_theme, 188 | "icon_theme": selected_icon_theme, 189 | "xfwm4_theme": selected_xfwm4_theme, 190 | "cursor_theme": selected_cursor_theme, 191 | "font": selected_font, 192 | "color_scheme": selected_color_scheme, 193 | "background": selected_background 194 | } 195 | 196 | desktop_dir = os.path.join(config_dir, "xfce") 197 | os.makedirs(desktop_dir, exist_ok=True) 198 | 199 | while True: 200 | theme_name = str(input('Enter the rice name: ')) 201 | 202 | if os.path.exists(f'{desktop_dir}/{theme_name}.json'): 203 | print('Theme with that name already exists!') 204 | else: 205 | write_to_json(selections, theme_name, desktop_dir) 206 | break 207 | 208 | def write_to_json(theme_properties: dict, name, desktop_dir): 209 | json_file_path = os.path.join(desktop_dir, f'{name}.json') 210 | 211 | os.makedirs(desktop_dir, exist_ok=True) 212 | 213 | with open(json_file_path, 'w') as json_file: 214 | json.dump(theme_properties, json_file, indent=4) 215 | 216 | print(f'Written data to: {json_file_path}') 217 | 218 | def read_theme(name): 219 | json_file_path = os.path.join(config_dir, f'{name}.json') 220 | 221 | with open(json_file_path, 'r') as json_file: 222 | data = json.load(json_file) 223 | 224 | return data 225 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # install.sh - Installation and Uninstallation Script for QuickRice 4 | # Usage: 5 | # ./install.sh install --global # Install QuickRice globally 6 | # ./install.sh install --local # Install QuickRice locally 7 | # ./install.sh uninstall --global # Uninstall QuickRice from global installation 8 | # ./install.sh uninstall --local # Uninstall QuickRice from local installation 9 | 10 | # Function to display usage information 11 | usage() { 12 | echo "Usage:" 13 | echo " $0 install [--global|--local] # Install QuickRice globally or locally" 14 | echo " $0 uninstall [--global|--local] # Uninstall QuickRice from global or local installation" 15 | echo " $0 help # Display this help message" 16 | exit 1 17 | } 18 | 19 | # Function to check if a command exists 20 | command_exists() { 21 | command -v "$1" >/dev/null 2>&1 22 | } 23 | 24 | # Function to handle errors 25 | handle_error() { 26 | echo "Error: $1" 27 | exit 1 28 | } 29 | 30 | # Function to install QuickRice 31 | install_quickrice() { 32 | INSTALL_TYPE=$1 33 | 34 | # Dependency Checks 35 | REQUIRED_COMMANDS=("mkdir" "cp" "chmod" "ln" "rm" "sudo") 36 | for cmd in "${REQUIRED_COMMANDS[@]}"; do 37 | if ! command_exists "$cmd"; then 38 | handle_error "Required command '$cmd' is not available. Please install it and retry." 39 | fi 40 | done 41 | 42 | # Determine script directory for absolute paths 43 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 44 | 45 | if [ "$INSTALL_TYPE" == "--global" ]; then 46 | INSTALL_DIR="/opt/quickrice" 47 | SYMLINK_DIR="/usr/local/bin" 48 | SYMLINK_PATH="$SYMLINK_DIR/quickrice" 49 | NEED_SUDO=true 50 | 51 | # Check if the user has sudo privileges 52 | if ! sudo -v >/dev/null 2>&1; then 53 | echo "Error: Global installation requires sudo privileges." 54 | exit 1 55 | fi 56 | 57 | # Remove existing local symlink if installing globally 58 | LOCAL_SYMLINK_PATH="$HOME/.local/bin/quickrice" 59 | if [ -L "$LOCAL_SYMLINK_PATH" ]; then 60 | echo "Removing existing local symlink at $LOCAL_SYMLINK_PATH..." 61 | rm "$LOCAL_SYMLINK_PATH" || handle_error "Failed to remove local symlink." 62 | fi 63 | 64 | elif [ "$INSTALL_TYPE" == "--local" ]; then 65 | INSTALL_DIR="$HOME/.quickrice" 66 | SYMLINK_DIR="$HOME/.local/bin" 67 | SYMLINK_PATH="$SYMLINK_DIR/quickrice" 68 | NEED_SUDO=false 69 | 70 | # Remove existing global symlink if installing locally 71 | GLOBAL_SYMLINK_PATH="/usr/local/bin/quickrice" 72 | if [ -L "$GLOBAL_SYMLINK_PATH" ]; then 73 | echo "Removing existing global symlink at $GLOBAL_SYMLINK_PATH..." 74 | sudo rm "$GLOBAL_SYMLINK_PATH" || handle_error "Failed to remove global symlink." 75 | fi 76 | else 77 | echo "Error: Please specify --global or --local for installation." 78 | usage 79 | fi 80 | 81 | # Check if installation directory already exists 82 | if [ -d "$INSTALL_DIR" ]; then 83 | echo "QuickRice is already installed in $INSTALL_DIR." 84 | read -p "Do you want to reinstall QuickRice? (y/n): " REINSTALL 85 | if [ "$REINSTALL" != "y" ]; then 86 | echo "Installation aborted." 87 | exit 0 88 | fi 89 | echo "Reinstalling QuickRice..." 90 | # Remove the existing installation directory 91 | if [ "$NEED_SUDO" = true ]; then 92 | sudo rm -rf "$INSTALL_DIR" || handle_error "Failed to remove existing installation directory." 93 | else 94 | rm -rf "$INSTALL_DIR" || handle_error "Failed to remove existing installation directory." 95 | fi 96 | fi 97 | 98 | # Create the installation directory 99 | echo "Creating installation directory at $INSTALL_DIR..." 100 | if [ "$NEED_SUDO" = true ]; then 101 | sudo mkdir -p "$INSTALL_DIR" || handle_error "Failed to create installation directory." 102 | else 103 | mkdir -p "$INSTALL_DIR" || handle_error "Failed to create installation directory." 104 | fi 105 | 106 | # Copy all project files to the installation directory 107 | echo "Copying project files to $INSTALL_DIR..." 108 | if [ "$NEED_SUDO" = true ]; then 109 | sudo cp -r "$SCRIPT_DIR/quickrice/"* "$INSTALL_DIR/" || handle_error "Failed to copy project files." 110 | else 111 | cp -r "$SCRIPT_DIR/quickrice/"* "$INSTALL_DIR/" || handle_error "Failed to copy project files." 112 | fi 113 | 114 | # Ensure the main.py script is executable 115 | echo "Setting executable permissions for main.py..." 116 | if [ "$NEED_SUDO" = true ]; then 117 | sudo chmod +x "$INSTALL_DIR/main.py" || handle_error "Failed to set executable permissions." 118 | else 119 | chmod +x "$INSTALL_DIR/main.py" || handle_error "Failed to set executable permissions." 120 | fi 121 | 122 | # Create the symlink 123 | echo "Creating symlink at $SYMLINK_PATH -> $INSTALL_DIR/main.py..." 124 | if [ -L "$SYMLINK_PATH" ]; then 125 | echo "Symlink $SYMLINK_PATH already exists. Removing it..." 126 | if [ "$NEED_SUDO" = true ]; then 127 | sudo rm "$SYMLINK_PATH" || handle_error "Failed to remove existing symlink." 128 | else 129 | rm "$SYMLINK_PATH" || handle_error "Failed to remove existing symlink." 130 | fi 131 | fi 132 | 133 | if [ "$INSTALL_TYPE" == "--global" ]; then 134 | sudo ln -s "$INSTALL_DIR/main.py" "$SYMLINK_PATH" || handle_error "Failed to create symlink." 135 | else 136 | # Create ~/.local/bin if it doesn't exist 137 | mkdir -p "$SYMLINK_DIR" || handle_error "Failed to create symlink directory." 138 | 139 | # Check if ~/.local/bin is in PATH 140 | if [[ ":$PATH:" != *":$SYMLINK_DIR:"* ]]; then 141 | echo "Adding $SYMLINK_DIR to PATH in ~/.bashrc..." 142 | echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.bashrc" 143 | echo "Please reload your shell or run 'source ~/.bashrc' to apply the changes." 144 | fi 145 | 146 | ln -s "$INSTALL_DIR/main.py" "$SYMLINK_PATH" || handle_error "Failed to create symlink." 147 | fi 148 | 149 | echo "QuickRice has been successfully installed." 150 | } 151 | 152 | # Function to uninstall QuickRice 153 | uninstall_quickrice() { 154 | INSTALL_TYPE=$1 155 | 156 | # Dependency Checks 157 | REQUIRED_COMMANDS=("rm" "sudo" "ln") 158 | for cmd in "${REQUIRED_COMMANDS[@]}"; do 159 | if ! command_exists "$cmd"; then 160 | handle_error "Required command '$cmd' is not available. Please install it and retry." 161 | fi 162 | done 163 | 164 | if [ "$INSTALL_TYPE" == "--global" ]; then 165 | INSTALL_DIR="/opt/quickrice" 166 | SYMLINK_DIR="/usr/local/bin" 167 | SYMLINK_PATH="$SYMLINK_DIR/quickrice" 168 | NEED_SUDO=true 169 | elif [ "$INSTALL_TYPE" == "--local" ]; then 170 | INSTALL_DIR="$HOME/.quickrice" 171 | SYMLINK_DIR="$HOME/.local/bin" 172 | SYMLINK_PATH="$SYMLINK_DIR/quickrice" 173 | NEED_SUDO=false 174 | else 175 | echo "Error: Please specify --global or --local for uninstallation." 176 | usage 177 | fi 178 | 179 | # Check if installation directory exists 180 | if [ ! -d "$INSTALL_DIR" ]; then 181 | echo "QuickRice is not installed in $INSTALL_DIR." 182 | exit 0 183 | fi 184 | 185 | # Remove the symlink 186 | if [ -L "$SYMLINK_PATH" ]; then 187 | echo "Removing symlink at $SYMLINK_PATH..." 188 | if [ "$INSTALL_TYPE" == "--global" ]; then 189 | if [ "$(id -u)" -ne 0 ]; then 190 | sudo rm "$SYMLINK_PATH" || handle_error "Failed to remove symlink." 191 | else 192 | rm "$SYMLINK_PATH" || handle_error "Failed to remove symlink." 193 | fi 194 | else 195 | rm "$SYMLINK_PATH" || handle_error "Failed to remove symlink." 196 | fi 197 | else 198 | echo "Symlink $SYMLINK_PATH does not exist." 199 | fi 200 | 201 | # Remove the installation directory 202 | echo "Removing installation directory at $INSTALL_DIR..." 203 | if [ "$INSTALL_TYPE" == "--global" ]; then 204 | sudo rm -rf "$INSTALL_DIR" || handle_error "Failed to remove installation directory." 205 | else 206 | rm -rf "$INSTALL_DIR" || handle_error "Failed to remove installation directory." 207 | fi 208 | 209 | echo "QuickRice has been successfully uninstalled." 210 | } 211 | 212 | # Check if at least one argument is provided 213 | if [ $# -lt 1 ]; then 214 | usage 215 | fi 216 | 217 | # Parse the first argument 218 | ACTION=$1 219 | 220 | case "$ACTION" in 221 | install) 222 | if [ $# -ne 2 ]; then 223 | echo "Error: Missing option for install." 224 | usage 225 | fi 226 | OPTION=$2 227 | install_quickrice "$OPTION" 228 | ;; 229 | uninstall) 230 | if [ $# -ne 2 ]; then 231 | echo "Error: Missing option for uninstall." 232 | usage 233 | fi 234 | OPTION=$2 235 | uninstall_quickrice "$OPTION" 236 | ;; 237 | help) 238 | usage 239 | ;; 240 | *) 241 | echo "Error: Unknown action '$ACTION'." 242 | usage 243 | ;; 244 | esac --------------------------------------------------------------------------------