From e883fb3be0a9f75b87bfdb7178211eeab73038af Mon Sep 17 00:00:00 2001 From: Abdullah Muhamed Date: Wed, 25 Dec 2024 14:25:36 +0200 Subject: [PATCH 1/3] Gemini Pro Vision Support --- .../.configuration/user_options.default.json | 484 +++++++++++++++--- .config/ags/modules/bar/modules/utils.js | 150 ++++++ .config/ags/modules/sideleft/apis/gemini.js | 207 ++++++-- .config/ags/modules/sideleft/apiwidgets.js | 97 +++- .config/ags/services/gemini.js | 311 ++++++++--- .config/hypr/hyprland/keybinds.conf | 2 +- 6 files changed, 1058 insertions(+), 193 deletions(-) create mode 100644 .config/ags/modules/bar/modules/utils.js diff --git a/.config/ags/modules/.configuration/user_options.default.json b/.config/ags/modules/.configuration/user_options.default.json index ec7f12aee..6db1685f7 100644 --- a/.config/ags/modules/.configuration/user_options.default.json +++ b/.config/ags/modules/.configuration/user_options.default.json @@ -1,7 +1,43 @@ { + "enabled": true, + "opacity": 0, + "defaultBarMode": 1, + "modules": { + "bar": true, + "dock": false, + "sideleft": true, + "sideright": true, + "overview": true, + "cheatsheet": true, + "indicators": false, + "osd": false, + "powermenu": true, + "settings": false, + "session": true, + "colorscheme": true, + "screencorners": true, + "desktopbackground": false, + "wallselect": true, + "onscreenkeyboard": false, + "crosshair": false, + "notifications": true, + "weather": false, + "system": false, + "media": true, + "clock": true, + "workspaces": true, + "tray": true, + "battery": true, + "network": true, + "volume": true, + "keyboard": true, + "quran": { + "verseNumberStyle": "circle" + } + }, "ai": { "defaultGPTProvider": "openrouter", - "onSearch": "gemini", + "onSearch": "openrouter", "defaultTemperature": 0.9, "enhancements": true, "useHistory": true, @@ -12,7 +48,11 @@ "animations": { "choreographyDelay": 25, "durationSmall": 110, - "durationLarge": 180 + "durationLarge": 180, + "enabled": true, + "choreography": true, + "duration": 200, + "curve": "ease-out" }, "appearance": { "autoDarkMode": { @@ -23,8 +63,27 @@ "keyboardUseFlag": false, "layerSmoke": false, "layerSmokeStrength": 0.2, - "barRoundCorners": 1, - "fakeScreenRounding": 1 + "barRoundCorners": 0, + "fakeScreenRounding": 0, + "visualizer": {}, + "profilePhoto": "", + "theme": { + "name": "default", + "style": "dark", + "blur": { + "enabled": true, + "strength": 5 + }, + "colors": { + "primary": "#5294e2", + "secondary": "#7c818c", + "success": "#73d216", + "warning": "#f57900", + "error": "#cc0000", + "background": "#2f343f", + "foreground": "#d3dae3" + } + } }, "apps": { "bluetooth": "blueberry", @@ -32,19 +91,37 @@ "network": "XDG_CURRENT_DESKTOP=\"gnome\" gnome-control-center wifi", "settings": "XDG_CURRENT_DESKTOP=\"gnome\" gnome-control-center", "taskManager": "foot -e btop", - "terminal": "foot" + "terminal": "foot", + "browser": "firefox", + "fileManager": "nautilus", + "textEditor": "code", + "audioPlayer": "spotify", + "calculator": "gnome-calculator" }, "battery": { "low": 20, "critical": 10, - "warnLevels": [20, 15, 5], - "warnTitles": ["Low battery", "Very low battery", "Critical Battery"], + "warnLevels": [ + 20, + 15, + 5 + ], + "warnTitles": [ + "Low battery", + "Very low battery", + "Critical Battery" + ], "warnMessages": [ "Plug in the charger", "You there?", "PLUG THE CHARGER ALREADY" ], - "suspendThreshold": 3 + "suspendThreshold": 3, + "batteryBar": { + "enabled": true, + "showPercentage": true, + "showTime": true + } }, "brightness": { "controllers": { @@ -73,18 +150,30 @@ "preferredPlayer": "plasma-browser-integration" }, "onScreenKeyboard": { - "layout": "qwerty_full" + "layout": "qwerty_full", + "enabled": true }, "overview": { "scale": 0.18, "numOfRows": 2, "numOfCols": 5, "wsNumScale": 0.09, - "wsNumMarginScale": 0.07 + "wsNumMarginScale": 0.07, + "enabled": true, + "layout": "grid", + "scrolling": true, + "preview": { + "enabled": true, + "scale": 0.18, + "rows": 2, + "cols": 3 + } }, "sidebar": { "ai": { - "__custom": ["extraGptModels"], + "__custom": [ + "extraGptModels" + ], "extraGptModels": { "openrouter": { "name": "Llama 3 8B", @@ -105,19 +194,29 @@ "saveInFolderByTags": false }, "pages": { - "order": ["apis", "tools", "github"], + "order": [ + "apis", + "tools" + ], "apis": { - "order": ["gpt", "translater"] + "order": [ + "gemini", + "gpt", + "translater", + "quran" + ] } }, "translater": { - "__custom": ["languages"], + "__custom": [ + "languages" + ], "from": "auto", - "to": "en", + "to": "ar", "languages": { "auto": "Auto", "en": "English", - "ru": "Russian" + "ar": "Arabic" } } }, @@ -136,65 +235,67 @@ "webSearch": true }, "engineBaseUrl": "https://www.google.com/search?q=", - "excludedSites": ["quora.com"] + "excludedSites": [ + "quora.com" + ] }, "time": { - "format": "%H:%M", - "interval": 5000, + "format": "%I:%M", + "interval": 1000, "dateFormatLong": "%A, %d/%m", - "dateInterval": 5000, + "dateInterval": 500000, "dateFormat": "%d/%m" }, - "weather": { - "city": "Barabinsk", - "preferredUnit": "C" - }, - "workspaces": { - "shown": 10 - }, - "dock": { - "enabled": false, - "hiddenThickness": 5, - "pinnedApps": ["firefox", "org.gnome.Nautilus"], - "layer": "top", - "monitorExclusivity": true, - "searchPinnedAppIcons": false, - "trigger": ["client-added", "client-removed"], - "autoHide": [ + "timers": { + "presets": [ { - "trigger": "client-added", - "interval": 500 + "name": "Pomodoro", + "duration": 1500, + "icon": "timer" }, { - "trigger": "client-removed", - "interval": 500 + "name": "Short Break", + "duration": 300, + "icon": "coffee" + }, + { + "name": "Long Break", + "duration": 900, + "icon": "self_improvement" } ] }, - "icons": { - "__custom": ["substitutions", "regexSubstitutions"], - "searchPaths": [""], - "symbolicIconTheme": { - "dark": "Adwaita", - "light": "Adwaita" - }, - "substitutions": { - "code-url-handler": "code", - "Code": "code", - "GitHub Desktop": "github-desktop", - "Minecraft* 1.20.1": "minecraft", - "gnome-tweaks": "org.gnome.tweaks", - "pavucontrol-qt": "pavucontrol", - "wps": "wps-office2019-kprometheus", - "wpsoffice": "wps-office2019-kprometheus", - "": "image-missing" + "weather": { + "city": "Cairo", + "preferredUnit": "C", + "enabled": true, + "provider": "openweathermap", + "location": "auto", + "units": "metric", + "showDetails": true, + "updateInterval": 900000 + }, + "workspaces": { + "shown": 10, + "labels": { + "1": "一", + "2": "二", + "3": "三", + "4": "四", + "5": "五", + "6": "六", + "7": "七", + "8": "八", + "9": "九", + "10": "十" }, - "regexSubstitutions": [ - { - "regex": {}, - "replace": "steam_icon_$1" - } - ] + "numeric_style": "unicode", + "defaultLabel": "•", + "activeWorkspaceStyle": "unicode", + "focus": 6, + "enabled": true, + "style": "unicode", + "duration": 200 }, "keybinds": { "overview": { @@ -226,9 +327,260 @@ "cycleTab": "Ctrl+Tab" } }, + "ytmusic": { + "audioQuality": { + "type": "enum", + "default": "low", + "enums": [ + "low", + "medium", + "high" + ], + "descriptions": { + "low": "Low quality (saves data)", + "medium": "Medium quality (128kbps)", + "high": "High quality (best opus/audio)" + } + }, + "autoQueue": { + "type": "boolean", + "default": true, + "description": "Automatically queue similar tracks" + }, + "queueSize": { + "type": "number", + "default": 5, + "description": "Number of tracks to auto-queue" + }, + "cacheTimeout": { + "type": "number", + "default": 30, + "description": "Cache timeout in minutes" + } + }, + "widget": { + "enabled": true, + "name": "ags-shell" + }, + "display": { + "enabled": true, + "expand": true, + "scale-factor": 1 + }, + "keyboard": { + "enabled": true, + "layout": "us", + "numlock": true, + "capslock": false, + "repeat": { + "delay": 500, + "interval": 30 + } + }, + "panel": { + "enabled": true, + "position": "top", + "spacing": 8, + "height": 32, + "margin": { + "top": 0, + "bottom": 0, + "left": 0, + "right": 0 + }, + "padding": { + "top": 0, + "bottom": 0, + "left": 8, + "right": 8 + } + }, + "presets": { + "minimal": { + "bar": true, + "dock": false, + "sideleft": false, + "sideright": true, + "notifications": true, + "quicksettings": true + }, + "gaming": { + "bar": true, + "dock": false, + "sideleft": false, + "sideright": false, + "notifications": false, + "quicksettings": false + }, + "full": { + "bar": true, + "dock": true, + "sideleft": true, + "sideright": true, + "notifications": true, + "quicksettings": true + } + }, + "gestures": { + "enabled": true, + "workspace": { + "swipeLeft": "next", + "swipeRight": "prev", + "pinchThreshold": 0.5 + }, + "overview": { + "pinchThreshold": 0.5 + } + }, + "icons": { + "__custom": [ + "substitutions", + "regexSubstitutions" + ], + "searchPaths": [ + "" + ], + "symbolicIconTheme": { + "dark": "Adwaita", + "light": "Adwaita" + }, + "substitutions": { + "code-url-handler": "code", + "Code": "code", + "GitHub Desktop": "github-desktop", + "Minecraft* 1.20.1": "minecraft", + "gnome-tweaks": "org.gnome.tweaks", + "pavucontrol-qt": "pavucontrol", + "wps": "wps-office2019-kprometheus", + "wpsoffice": "wps-office2019-kprometheus", + "": "image-missing" + }, + "regexSubstitutions": [ + { + "regex": "^steam_app_(\\d+)$", + "replace": "steam_icon_$1" + } + ] + }, + "dock": { + "enabled": true, + "hiddenThickness": 5, + "pinnedApps": [ + "zen-browser", + "spotube", + "github-desktop", + "visual-studio-code", + "zeditor", + "nwg-look", + "Obisidian", + "steam", + "heroic-games-launcher", + "org.gnome.Nautilus", + "chromium", + "visual studio code", + "windsurf" + ], + "layer": "top", + "monitorExclusivity": true, + "searchPinnedAppIcons": false, + "trigger": [ + "client-added", + "client-removed" + ], + "autoHide": [ + { + "trigger": "client-added", + "interval": 500 + }, + { + "trigger": "client-removed", + "interval": 500 + } + ], + "position": "bottom", + "iconSize": 48, + "margin": 8, + "spacing": 8, + "behavior": { + "pressure": true, + "autohide": true, + "showUnpinned": true, + "hideTimeout": 500 + } + }, "bar": { - "modes": ["normal"], + "mode": "mode1", "wallpaper_folder": "~/Pictures/Wallpapers", - "position": "top" + "position": "top", + "enabled": true, + "spacing": 8, + "elements": { + "showWorkspaces": true, + "showBattery": true, + "showClock": true, + "showNetwork": true, + "showVolume": true, + "showPower": true, + "showNotifications": true, + "showSystemTray": true + } + }, + "notifications": { + "position": "top right", + "blacklist": [ + "Spotify" + ], + "enabled": true, + "width": 380, + "timeout": 5000, + "popupTimeout": 5000, + "maxVisible": 5, + "spacing": 8 + }, + "quicksettings": { + "enabled": true, + "position": "right", + "media": { + "enabled": true, + "preferredPlayer": "spotify", + "coverArt": { + "enabled": true, + "size": 96 + } + }, + "sliders": { + "volume": { + "enabled": true, + "showIcon": true + }, + "brightness": { + "enabled": true, + "showIcon": true + } + } + }, + "power": { + "enabled": true, + "profile": "balanced" + }, + "sideright": { + "enabled": true, + "width": 400, + "position": "right", + "modules": { + "quicksettings": true, + "notifications": true, + "media": true + } + }, + "sideleft": { + "enabled": true, + "width": 400, + "position": "left", + "modules": { + "dashboard": true, + "music": true, + "weather": true + } } -} +} \ No newline at end of file diff --git a/.config/ags/modules/bar/modules/utils.js b/.config/ags/modules/bar/modules/utils.js new file mode 100644 index 000000000..d25636974 --- /dev/null +++ b/.config/ags/modules/bar/modules/utils.js @@ -0,0 +1,150 @@ +import Widget from "resource:///com/github/Aylur/ags/widget.js"; +import * as Utils from "resource:///com/github/Aylur/ags/utils.js"; +import App from "resource:///com/github/Aylur/ags/app.js"; +const { Box, Button } = Widget; +const { GLib } = imports.gi; + +const createUtilButton = ({ name, icon, onClicked, onSecondaryClick }) => { + const buttonProps = { + vpack: "center", + tooltipText: name, + onClicked, + className: "icon-material sec-txt txt-larger", + label: icon, // No need for template literal here + }; + + if (onSecondaryClick) { + buttonProps.onSecondaryClick = onSecondaryClick; + } + + return Button(buttonProps); +}; + +const createNerdButton = ({ name, icon, onClicked, onSecondaryClick }) => { + const buttonProps = { + vpack: "center", + tooltipText: name, + onClicked, + className: "icon-nerd sec-txt txt-title", + label: icon, // No need for template literal here + }; + + if (onSecondaryClick) { + buttonProps.onSecondaryClick = onSecondaryClick; + } + + return Button(buttonProps); +}; + +let wallpaperFolder = ""; + +const changeWallpaperButton = () => createUtilButton({ + name: "Change wallpaper", + icon: "image", + onClicked: () => Utils.execAsync([ + `${App.configDir}/scripts/color_generation/randomwall.sh` + ]), + onSecondaryClick: () => App.toggleWindow("wallselect"), +}); + +const geminiScreenshotButton = () => createUtilButton({ + name: "Analyze screenshot with AI", + icon: "screenshot_region", + onClicked: () => { + const { sendScreenshotToGemini } = globalThis; + if (sendScreenshotToGemini) { + sendScreenshotToGemini(); + } else { + Utils.execAsync(['notify-send', 'Error', 'Screenshot to Gemini not available']); + } + }, +}); + +const Shortcuts = () => { + let unsubscriber = () => {}; + let showWallpaperButton = false; + + const unixporn = createUtilButton({ + name: "Unix Porn", + css:"font-size:1.8rem", + icon: "\udb81\udfea", + onClicked: () => Utils.execAsync(`windsurf ${App.configDir}`), + onSecondaryClicked: () => Utils.execAsync(`xdg-open "https://www.reddit.com/r/unixporn/"`), + }); + + const collage = createUtilButton({ + name: "LMS site", + icon: "ecg_heart", + onClicked: () => Utils.execAsync(`firefox --new-window http://lms.nv.edu.eg`), + }); + + const gitHubButton = createNerdButton({ + name: "GitHub", + icon: "\uea84", + onClicked: () => Utils.execAsync(`firefox --new-window github.com/pharmaracist`), + }); + + const yt = createNerdButton({ + name: "YT", + icon: "\uf166", + onClicked: () => Utils.execAsync(`firefox --new-window youtube.com`), + }); + + const agsTweaksButton = createUtilButton({ + name: "Settings", + icon: "water_drop", + onClicked: () => Utils.execAsync([ + "bash", + "-c", + `${GLib.get_home_dir()}/.local/bin/ags-tweaks`, + ]), + }); + + const screenSnipButton = createUtilButton({ + name: "Screen snip", + icon: "screenshot_region", + onClicked: () => Utils.execAsync(`${App.configDir}/scripts/grimblast.sh copy area`).catch(print), + }); + + const colorPickerButton = createUtilButton({ + name: "Color picker", + icon: "colorize", + onClicked: () => Utils.execAsync(["hyprpicker", "-a"]).catch(print), + }); + + const box = Box({ + className: "spacing-h-5", + children: [ + geminiScreenshotButton(), + yt, + agsTweaksButton, + gitHubButton, + unixporn, + collage, + // screenSnipButton, + colorPickerButton, + changeWallpaperButton(), + ], + }); + + unsubscriber = userOptions.subscribe((options) => { + wallpaperFolder = options.bar.wallpaper_folder; + const shouldShow = typeof wallpaperFolder === "string"; + + if (shouldShow !== showWallpaperButton) { + showWallpaperButton = shouldShow; + if (shouldShow) { + box.add(changeWallpaperButton()); + } else { + box.remove(changeWallpaperButton()); + } + } + }); + + box.on("destroy", unsubscriber); + + return box; +}; + +export default Shortcuts; +export { changeWallpaperButton, wallpaperFolder }; \ No newline at end of file diff --git a/.config/ags/modules/sideleft/apis/gemini.js b/.config/ags/modules/sideleft/apis/gemini.js index 5d68e4364..8424d94fe 100644 --- a/.config/ags/modules/sideleft/apis/gemini.js +++ b/.config/ags/modules/sideleft/apis/gemini.js @@ -1,7 +1,10 @@ -const { Gtk, Pango } = imports.gi; +const { Gtk, Pango, Gdk } = imports.gi; import App from 'resource:///com/github/Aylur/ags/app.js'; import Widget from 'resource:///com/github/Aylur/ags/widget.js'; +import Service from 'resource:///com/github/Aylur/ags/service.js'; import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'; +import { fileExists } from '../../.miscutils/files.js'; +import GLib from 'gi://GLib'; const { Box, Button, Icon, Label, Revealer, Scrollable } = Widget; import GeminiService from '../../../services/gemini.js'; @@ -198,25 +201,151 @@ const CommandButton = (command) => Button({ label: command, }); -export const geminiCommands = Box({ - className: 'spacing-h-5', - children: [ - Box({ hexpand: true }), - CommandButton('/key'), - CommandButton('/model'), - CommandButton('/clear'), - ] +const ScreenshotButton = () => Button({ + className: 'sidebar-chat-chip sidebar-chat-chip-action txt-small', + setup: setupCursorHover, + tooltipText: 'Take a screenshot and analyze it', + child: Box({ + children: [ + Icon({ + icon: 'image-area', + size: 14, + }), + Label({ + label: ' Analyze Area', + }), + ] + }), + onClicked: () => { + const tempDir = GLib.get_tmp_dir(); + const timestamp = new Date().getTime(); + const tempPath = GLib.build_filenamev([tempDir, `gemini_screenshot_${timestamp}.png`]); + + chatContent.add(SystemMessage('Select an area to analyze...', 'Screenshot', geminiView)); + + Utils.execAsync(['bash', '-c', `grim -g "$(slurp)" "${tempPath}"`]) + .then(() => { + if (fileExists(tempPath)) { + GeminiService.sendWithImage('What can you tell me about this screenshot?', tempPath) + .catch(error => { + chatContent.add(SystemMessage(`Error processing screenshot: ${error.message}`, 'Error', geminiView)); + }) + .finally(() => { + // Clean up temp file after a delay + Utils.timeout(5000, () => { + try { + GLib.unlink(tempPath); + } catch (e) { + console.error('Error cleaning up temp file:', e); + } + }); + }); + } else { + chatContent.add(SystemMessage('Screenshot cancelled or failed', 'Error', geminiView)); + } + }) + .catch(error => { + chatContent.add(SystemMessage(`Screenshot failed: ${error}`, 'Error', geminiView)); + }); + }, }); +const handleClipboardImage = () => { + const clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD); + if (clipboard.wait_is_image_available()) { + const pixbuf = clipboard.wait_for_image(); + if (!pixbuf) { + chatContent.add(SystemMessage('No image found in clipboard', 'Error', geminiView)); + return false; + } + + // Save the image to a temporary file + const tempDir = GLib.get_tmp_dir(); + const timestamp = new Date().getTime(); + const tempPath = GLib.build_filenamev([tempDir, `gemini_clipboard_${timestamp}.png`]); + + try { + pixbuf.savev(tempPath, 'png', [], []); + return tempPath; + } catch (error) { + chatContent.add(SystemMessage(`Error saving clipboard image: ${error.message}`, 'Error', geminiView)); + return false; + } + } + return false; +}; + export const sendMessage = (text) => { - // Check if text or API key is empty - if (text.length == 0) return; + // Ensure text is a string and not empty + if (!text || typeof text !== 'string' || text.length === 0) return; + + // Clear text entry buffer + const buffer = chatEntry.get_buffer(); + buffer.set_text('', 0); + if (GeminiService.key.length == 0) { GeminiService.key = text; chatContent.add(SystemMessage(`Key saved to\n\`${GeminiService.keyPath}\``, 'API Key', geminiView)); text = ''; return; } + + // Handle image upload command + if (text.startsWith('/image')) { + const parts = text.split(' '); + + // Special handling for clipboard + if (parts[1] === 'clipboard' || parts[1] === '/Pasted') { + const tempPath = handleClipboardImage(); + if (tempPath) { + const description = parts.slice(2).join(' ') || 'Please analyze this image.'; + GeminiService.sendWithImage(description, tempPath) + .catch(error => { + chatContent.add(SystemMessage(`Error processing image: ${error.message}`, 'Error', geminiView)); + }) + .finally(() => { + // Clean up temp file after a delay + Utils.timeout(5000, () => { + try { + GLib.unlink(tempPath); + } catch (e) { + console.error('Error cleaning up temp file:', e); + } + }); + }); + } + return; + } + + // Regular file handling + if (parts.length < 2) { + chatContent.add(SystemMessage('Usage:\n`/image PATH_TO_IMAGE [description]`\n`/image clipboard [description]`', '/image', geminiView)); + return; + } + + let imagePath = parts[1]; + // Handle home directory expansion + if (imagePath.startsWith('~')) { + imagePath = imagePath.replace('~', GLib.get_home_dir()); + } + // Convert to absolute path if relative + if (!imagePath.startsWith('/')) { + imagePath = GLib.build_filenamev([GLib.get_current_dir(), imagePath]); + } + + if (!fileExists(imagePath)) { + chatContent.add(SystemMessage(`Image file not found: ${imagePath}\nMake sure the file exists and the path is correct.`, 'Error', geminiView)); + return; + } + + const description = parts.slice(2).join(' ') || 'Please analyze this image.'; + GeminiService.sendWithImage(description, imagePath) + .catch(error => { + chatContent.add(SystemMessage(`Error processing image: ${error.message}`, 'Error', geminiView)); + }); + return; + } + // Commands if (text.startsWith('/')) { if (text.startsWith('/clear')) clearChat(); @@ -248,6 +377,22 @@ export const sendMessage = (text) => { } else if (text.startsWith('/test')) chatContent.add(SystemMessage(markdownTest, `Markdown test`, geminiView)); + else if (text.startsWith('/help')) { + chatContent.add(SystemMessage( + 'Available commands:\n' + + '`/clear` - Clear chat history\n' + + '`/image PATH [description]` - Analyze an image file\n' + + '`/image clipboard [description]` - Analyze image from clipboard\n' + + '`/key [API_KEY]` - View or update API key\n' + + '`/model` - Show current model\n' + + '`/prompt MESSAGE` - Add a message without sending\n\n' + + 'You can also:\n' + + '• Click the camera button to select and analyze a screen area\n' + + '• Paste an image directly into the chat', + '/help', + geminiView + )); + } else chatContent.add(SystemMessage(getString(`Invalid command.`), 'Error', geminiView)) } @@ -256,37 +401,15 @@ export const sendMessage = (text) => { } } -// export const geminiView = Box({ -// homogeneous: true, -// children: [Scrollable({ -// className: 'sidebar-chat-viewport', -// vexpand: true, -// child: Box({ -// vertical: true, -// children: [ -// geminiWelcome, -// chatContent, -// ] -// }), -// setup: (scrolledWindow) => { -// // Show scrollbar -// scrolledWindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC); -// const vScrollbar = scrolledWindow.get_vscrollbar(); -// vScrollbar.get_style_context().add_class('sidebar-scrollbar'); -// // Avoid click-to-scroll-widget-to-view behavior -// Utils.timeout(1, () => { -// const viewport = scrolledWindow.child; -// viewport.set_focus_vadjustment(new Gtk.Adjustment(undefined)); -// }) -// // Always scroll to bottom with new content -// const adjustment = scrolledWindow.get_vadjustment(); -// adjustment.connect("changed", () => Utils.timeout(1, () => { -// if (!chatEntry.hasFocus) return; -// adjustment.set_value(adjustment.get_upper() - adjustment.get_page_size()); -// })) -// } -// })] -// }); +export const geminiCommands = Box({ + className: 'spacing-h-5', + children: [ + Box({ hexpand: true }), + CommandButton('/help'), + CommandButton('/clear'), + CommandButton('/key'), + ] +}); export const geminiView = Box({ homogeneous: true, diff --git a/.config/ags/modules/sideleft/apiwidgets.js b/.config/ags/modules/sideleft/apiwidgets.js index c9d37dec5..a97a38fd8 100644 --- a/.config/ags/modules/sideleft/apiwidgets.js +++ b/.config/ags/modules/sideleft/apiwidgets.js @@ -4,29 +4,58 @@ import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'; const { Box, Button, CenterBox, Entry, EventBox, Icon, Label, Overlay, Revealer, Scrollable, Stack } = Widget; const { execAsync, exec } = Utils; import { setupCursorHover, setupCursorHoverInfo } from '../.widgetutils/cursorhover.js'; +import { MaterialIcon } from '../.commonwidgets/materialicon.js'; +import { enableClickthrough } from "../.widgetutils/clickthrough.js"; +import { checkKeybind } from '../.widgetutils/keybind.js'; +import { widgetContent } from './sideleft.js'; +import { IconTabContainer } from '../.commonwidgets/tabcontainer.js'; +import { writable } from '../../modules/.miscutils/store.js'; +import { fileExists } from '../.miscutils/files.js'; +import GLib from 'gi://GLib'; +import { SystemMessage } from './apis/ai_chatmessage.js'; +const TextView = Widget.subclass(Gtk.TextView, "AgsTextView"); + // APIs import GPTService from '../../services/gpt.js'; import Gemini from '../../services/gemini.js'; +import YTMusic from '../../services/ytmusic.js'; +import QuranService from '../../services/quran.js'; +import WallpaperService from '../../services/wallpapers.js'; import { geminiView, geminiCommands, sendMessage as geminiSendMessage, geminiTabIcon } from './apis/gemini.js'; import { chatGPTView, chatGPTCommands, sendMessage as chatGPTSendMessage, chatGPTTabIcon } from './apis/chatgpt.js'; import { TranslaterView, translaterCommands, sendMessage as translaterSendMessage, translaterIcon } from './apis/translater.js'; -import { enableClickthrough } from "../.widgetutils/clickthrough.js"; -import { checkKeybind } from '../.widgetutils/keybind.js'; -const TextView = Widget.subclass(Gtk.TextView, "AgsTextView"); +import { ytmusicView, ytmusicCommands, sendMessage as ytmusicSendMessage, ytmusicTabIcon, MediaControls } from './apis/ytmusic.js'; +import { quranView, quranCommands, sendMessage as quranSendMessage, quranTabIcon } from './apis/quran.js'; +import { wallpaperView, wallpaperCommands, sendMessage as wallpaperSendMessage, wallpaperTabIcon } from './apis/wallpapers.js'; -import { widgetContent } from './sideleft.js'; -import { IconTabContainer } from '../.commonwidgets/tabcontainer.js'; -import { writable } from '../../modules/.miscutils/store.js'; +// Create a custom icon for YouTube Music const EXPAND_INPUT_THRESHOLD = 30; const APILIST = { + 'wallpapers': { + name: 'Wallpapers', + sendCommand: wallpaperSendMessage, + contentWidget: wallpaperView, + commandBar: wallpaperCommands, + tabIcon: wallpaperTabIcon, + placeholderText: 'Describe your dream wallpaper...', + }, + 'quran': { + name: 'Quran', + sendCommand: quranSendMessage, + contentWidget: quranView, + commandBar: quranCommands, + tabIcon: quranTabIcon, + + placeholderText: 'وَقُل رَّبِّ زِدْنِي عِلْمًا' + }, 'gemini': { name: 'Assistant (Gemini Pro)', sendCommand: geminiSendMessage, contentWidget: geminiView, commandBar: geminiCommands, tabIcon: geminiTabIcon, - placeholderText: getString('Message Gemini...'), + placeholderText: 'Message Gemini...', }, 'gpt': { name: 'Assistant (GPTs)', @@ -34,7 +63,7 @@ const APILIST = { contentWidget: chatGPTView, commandBar: chatGPTCommands, tabIcon: chatGPTTabIcon, - placeholderText: getString('Message the model...'), + placeholderText: 'Message the model...', }, 'translater': { name: 'Google Translater', @@ -44,6 +73,14 @@ const APILIST = { commandBar: translaterCommands, placeholderText: 'Translate the text...' }, + 'ytmusic': { + name: 'YouTube Music', + sendCommand: ytmusicSendMessage, + contentWidget: ytmusicView, + commandBar: ytmusicCommands, + tabIcon: ytmusicTabIcon, + placeholderText: 'Search music...', +}, } let APIS = writable ([]); @@ -58,12 +95,16 @@ function apiSendMessage(textView) { // Get text const buffer = textView.get_buffer(); const [start, end] = buffer.get_bounds(); - const text = buffer.get_text(start, end, true).trimStart(); - if (!text || text.length == 0) return; - // Send - APIS.asyncGet()[currentApiId].sendCommand(text) - // Reset - buffer.set_text("", -1); + const text = buffer.get_text(start, end, true); + if (!text || text.trim().length === 0) return; + + // Only send if the current API has a sendCommand function + const currentApi = APIS.asyncGet()[currentApiId]; + if (currentApi && currentApi.sendCommand) { + currentApi.sendCommand(text.trim()); + } + + // Don't reset the buffer here - let the API handle it chatEntryWrapper.toggleClassName('sidebar-chat-wrapper-extended', false); chatEntry.set_valign(Gtk.Align.CENTER); } @@ -74,18 +115,13 @@ export const chatEntry = TextView({ acceptsTab: false, className: 'sidebar-chat-entry txt txt-smallie', setup: (self) => self - .hook(App, (self, currentName, visible) => { - if (visible && currentName === 'sideleft') { - self.grab_focus(); - } - }) .hook(GPTService, (self) => { if (APIS.asyncGet()[currentApiId].name != 'Assistant (GPTs)') return; - self.placeholderText = (GPTService.key.length > 0 ? getString('Message the model...') : getString('Enter API Key...')); + self.placeholderText = (GPTService.key.length > 0 ? 'Message the model...' : 'Enter API Key...'); }, 'hasKey') .hook(Gemini, (self) => { if (APIS.asyncGet()[currentApiId].name != 'Assistant (Gemini Pro)') return; - self.placeholderText = (Gemini.key.length > 0 ? getString('Message Gemini...') : getString('Enter Google AI API Key...')); + self.placeholderText = (Gemini.key.length > 0 ? 'Message Gemini...' : 'Enter Google AI API Key...'); }, 'hasKey') .on("key-press-event", (widget, event) => { // Swtich APIs with Tab @@ -152,11 +188,20 @@ const chatSendButton = Button({ label: 'arrow_upward', setup: setupCursorHover, onClicked: (self) => { - APIS.asyncGet()[currentApiId].sendCommand(chatEntry.get_buffer().text); + APIS.asyncGet()[currentApiId].sendCommand(chatEntry); chatEntry.get_buffer().set_text("", -1); }, }); +const chatScreenshotButton = Button({ + className: 'sidebar-chat-chip sidebar-chat-chip-action txt-small', + vpack: 'end', + setup: setupCursorHover, + tooltipText: 'Take a screenshot', + child: MaterialIcon('screenshot_region', 'small'), + onClicked: () => sendScreenshotToGemini(), +}); + const chatPlaceholder = Label({ className: 'txt-subtext txt-smallie margin-left-5', hpack: 'start', @@ -181,7 +226,13 @@ const textboxArea = Box({ // Entry area overlays: [chatPlaceholderRevealer], }), Box({ className: 'width-10' }), - chatSendButton, + Box({ + className: 'spacing-h-5', + children: [ + chatScreenshotButton, + chatSendButton, + ], + }), ] }); diff --git a/.config/ags/services/gemini.js b/.config/ags/services/gemini.js index 876a6cd8e..762fec2dc 100644 --- a/.config/ags/services/gemini.js +++ b/.config/ags/services/gemini.js @@ -44,7 +44,7 @@ function replaceapidom(URL) { } return URL; } -const CHAT_MODELS = ["gemini-1.5-flash"] +const CHAT_MODELS = ["gemini-1.5-flash", "gemini-1.5-flash"] // Using same model for both since it supports vision const ONE_CYCLE_COUNT = 3; class GeminiMessage extends Service { @@ -57,19 +57,29 @@ class GeminiMessage extends Service { 'content': ['string'], 'thinking': ['boolean'], 'done': ['boolean'], + 'hasImage': ['boolean'], }); } _role = ''; - _parts = [{ text: '' }]; + _parts = []; _thinking; _done = false; _rawData = ''; + _hasImage = false; - constructor(role, content, thinking = true, done = false) { + constructor(role, content, thinking = true, done = false, imageData = null) { super(); this._role = role; - this._parts = [{ text: content }]; + if (imageData) { + this._parts = [ + { text: content }, + { inlineData: { mimeType: 'image/jpeg', data: imageData } } + ]; + this._hasImage = true; + } else { + this._parts = [{ text: content }]; + } this._thinking = thinking; this._done = done; } @@ -83,33 +93,33 @@ class GeminiMessage extends Service { get role() { return this._role } set role(role) { this._role = role; this.emit('changed') } - get content() { - return this._parts.map(part => part.text).join(); - } + get content() { return this._parts[0].text } set content(content) { - this._parts = [{ text: content }]; - this.notify('content') - this.emit('changed') + if (this._hasImage) { + this._parts[0].text = content; + } else { + this._parts = [{ text: content }]; + } + this.notify('content'); + this.emit('changed'); } get parts() { return this._parts } - - get label() { return this._parserState.parsed + this._parserState.stack.join('') } + get hasImage() { return this._hasImage } get thinking() { return this._thinking } set thinking(value) { this._thinking = value; - this.notify('thinking') - this.emit('changed') + this.notify('thinking'); + this.emit('changed'); } addDelta(delta) { if (this.thinking) { this.thinking = false; this.content = delta; - } - else { - this.content += delta; + } else { + this.content = this.content + delta; } this.emit('delta', delta); } @@ -117,17 +127,23 @@ class GeminiMessage extends Service { parseSection() { if (this._thinking) { this.thinking = false; - this._parts[0].text = ''; + if (!this._hasImage) { + this._parts[0].text = ''; + } } - const parsedData = JSON.parse(this._rawData); - if (!parsedData.candidates) - this._parts[0].text += `Blocked: ${parsedData.promptFeedback.blockReason}`; - else { - const delta = parsedData.candidates[0].content.parts[0].text; - this._parts[0].text += delta; + try { + const parsedData = JSON.parse(this._rawData); + if (!parsedData.candidates) { + this._parts[0].text += `Error: ${parsedData.promptFeedback?.blockReason || 'Unknown error'}`; + } else { + const delta = parsedData.candidates[0].content.parts[0].text; + this._parts[0].text += delta; + } + this.notify('content'); + } catch (error) { + this._parts[0].text += 'Error parsing response'; + this.notify('content'); } - // this.emit('delta', delta); - this.notify('content'); this._rawData = ''; } } @@ -139,6 +155,7 @@ class GeminiService extends Service { 'clear': [], 'newMsg': ['int'], 'hasKey': ['boolean'], + 'imageProcessing': ['boolean'], }); } @@ -152,6 +169,7 @@ class GeminiService extends Service { _messages = []; _modelIndex = 0; _decoder = new TextDecoder(); + _processingImage = false; constructor() { super(); @@ -251,21 +269,69 @@ class GeminiService extends Service { (stream, res) => { try { const [bytes] = stream.read_line_finish(res); + if (!bytes) { + // Try to parse accumulated response + if (aiResponse._rawData) { + try { + const response = JSON.parse(aiResponse._rawData); + if (response.error) { + const errorMsg = `Error: ${response.error.message} (${response.error.status})`; + aiResponse.addDelta(errorMsg); + } + else if (response.candidates?.[0]?.content?.parts?.[0]?.text) { + const text = response.candidates[0].content.parts[0].text; + aiResponse.addDelta(text); + } + else { + aiResponse.addDelta('Error: Unexpected response format'); + } + } catch (e) { + aiResponse.addDelta('Error parsing response'); + } + } + + aiResponse.thinking = false; + aiResponse.done = true; + if (this._usingHistory) this.saveHistory(); + return; + } + const line = this._decoder.decode(bytes); - // console.log(line); - if (line == '[{') { // beginning of response - aiResponse._rawData += '{'; - this.thinking = false; + aiResponse._rawData = (aiResponse._rawData || '') + line; + + // Handle error responses immediately + if (line.includes('"error"')) { + try { + const errorResponse = JSON.parse(aiResponse._rawData + line + '}'); + const errorMsg = `Error: ${errorResponse.error.message} (${errorResponse.error.status})`; + aiResponse.addDelta(errorMsg); + aiResponse.thinking = false; + aiResponse.done = true; + return; + } catch (e) { + } } - else if (line == ',\u000d' || line == ']') { // end of stream pulse - aiResponse.parseSection(); + + // Try to parse complete response + if (line.trim().endsWith('}')) { + try { + const response = JSON.parse(aiResponse._rawData); + if (response.candidates?.[0]?.content?.parts?.[0]?.text) { + const text = response.candidates[0].content.parts[0].text; + aiResponse.addDelta(text); + aiResponse.thinking = false; + aiResponse.done = true; + return; + } + } catch (e) { + } } - else // Normal content - aiResponse._rawData += line; + // Continue reading this.readResponse(stream, aiResponse); - } catch { + } catch (error) { aiResponse.done = true; + aiResponse.addDelta('Error reading response: ' + error.message); if (this._usingHistory) this.saveHistory(); return; } @@ -280,53 +346,176 @@ class GeminiService extends Service { send(msg) { this._messages.push(new GeminiMessage('user', msg, false)); this.emit('newMsg', this._messages.length - 1); - const aiResponse = new GeminiMessage('model', 'thinking...', true, false) + const aiResponse = new GeminiMessage('model', 'thinking...', true, false); - const body = - { - "contents": this._messages.map(msg => { let m = { role: msg.role, parts: msg.parts }; return m; }), + const body = { + "contents": [{ + "role": "user", + "parts": [{ "text": msg }] + }], "safetySettings": this._safe ? [] : [ - // { category: "HARM_CATEGORY_DEROGATORY", threshold: "BLOCK_NONE", }, - { category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE", }, - { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE", }, - { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE", }, - // { category: "HARM_CATEGORY_UNSPECIFIED", threshold: "BLOCK_NONE", }, + { category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }, + { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE" }, + { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE" }, ], "generationConfig": { "temperature": this._temperature, }, - // "key": this._key, - // "apiKey": this._key, }; - const proxyResolver = new Gio.SimpleProxyResolver({ 'default-proxy': userOptions.asyncGet().ai.proxyUrl }); - const session = new Soup.Session({ 'proxy-resolver': proxyResolver }); + + const session = new Soup.Session(); + session.set_timeout(30); // 30 second timeout + + const proxyUrl = userOptions.asyncGet().ai.proxyUrl; + if (proxyUrl && proxyUrl.length > 0) { + const proxyResolver = new Gio.SimpleProxyResolver(); + proxyResolver.set_default_proxy(proxyUrl); + session.set_proxy_resolver(proxyResolver); + } + + const apiUrl = replaceapidom(`https://generativelanguage.googleapis.com/v1/models/${this.modelName}:generateContent?key=${this._key}`); + const message = new Soup.Message({ method: 'POST', - uri: GLib.Uri.parse(replaceapidom(`https://generativelanguage.googleapis.com/v1/models/${this.modelName}:streamGenerateContent?key=${this._key}`), GLib.UriFlags.NONE), + uri: GLib.Uri.parse(apiUrl, GLib.UriFlags.NONE), }); - message.request_headers.append('Content-Type', `application/json`); - message.set_request_body_from_bytes('application/json', new GLib.Bytes(JSON.stringify(body))); + + message.request_headers.append('Content-Type', 'application/json'); + const bodyBytes = new GLib.Bytes(JSON.stringify(body)); + message.set_request_body_from_bytes('application/json', bodyBytes); + + this._messages.push(aiResponse); + this.emit('newMsg', this._messages.length - 1); session.send_async(message, GLib.DEFAULT_PRIORITY, null, (_, result) => { try { const stream = session.send_finish(result); - this.readResponse(new Gio.DataInputStream({ + if (!stream) { + throw new Error('No response stream received'); + } + + const dataStream = new Gio.DataInputStream({ close_base_stream: true, base_stream: stream - }), aiResponse); + }); + + this.readResponse(dataStream, aiResponse); } catch (e) { - aiResponse.addDelta (e.message); - aiResponse.thinking = false; + aiResponse.done = true; + aiResponse.addDelta('Error in API response: ' + e.message); } }); - this._messages.push(aiResponse); - this.emit('newMsg', this._messages.length - 1); - if (this._cycleModels) { - this._requestCount++; - if (this._cycleModels) - this._modelIndex = (this._requestCount - (this._requestCount % ONE_CYCLE_COUNT)) % CHAT_MODELS.length; + if (this._cycleModels && ++this._requestCount % ONE_CYCLE_COUNT == 0) + this._modelIndex = (this._requestCount - (this._requestCount % ONE_CYCLE_COUNT)) % CHAT_MODELS.length; + } + + async processImage(imagePath) { + try { + this._processingImage = true; + this.emit('imageProcessing', true); + + const imageFile = Gio.File.new_for_path(imagePath); + const [success, contents] = imageFile.load_contents(null); + + if (!success || !contents) { + throw new Error('Failed to read image file'); + } + + const base64Data = GLib.base64_encode(contents); + return base64Data; + } catch (error) { + throw error; + } finally { + this._processingImage = false; + this.emit('imageProcessing', false); + } + } + + async sendWithImage(msg, imagePath) { + try { + const imageData = await this.processImage(imagePath); + + this._modelIndex = 0; + + const userMessage = new GeminiMessage('user', msg, false, false, imageData); + this._messages.push(userMessage); + this.emit('newMsg', this._messages.length - 1); + + const aiResponse = new GeminiMessage('model', 'Analyzing image...', true, false); + + const body = { + "contents": [{ + "role": userMessage.role, + "parts": [ + { + "text": msg + }, + { + "inline_data": { + "mime_type": "image/png", + "data": imageData + } + } + ] + }], + "safety_settings": this._safe ? [] : [ + { category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }, + { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE" }, + { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE" }, + { category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_NONE" } + ], + "generation_config": { + "temperature": this._temperature, + } + }; + + const session = new Soup.Session(); + session.set_timeout(30); // 30 second timeout + + const proxyUrl = userOptions.asyncGet().ai.proxyUrl; + if (proxyUrl && proxyUrl.length > 0) { + const proxyResolver = new Gio.SimpleProxyResolver(); + proxyResolver.set_default_proxy(proxyUrl); + session.set_proxy_resolver(proxyResolver); + } + + const apiUrl = replaceapidom(`https://generativelanguage.googleapis.com/v1/models/${this.modelName}:generateContent?key=${this._key}`); + + const message = new Soup.Message({ + method: 'POST', + uri: GLib.Uri.parse(apiUrl, GLib.UriFlags.NONE), + }); + + message.request_headers.append('Content-Type', 'application/json'); + const bodyBytes = new GLib.Bytes(JSON.stringify(body)); + message.set_request_body_from_bytes('application/json', bodyBytes); + + this._messages.push(aiResponse); + this.emit('newMsg', this._messages.length - 1); + + session.send_async(message, GLib.DEFAULT_PRIORITY, null, (_, result) => { + try { + const stream = session.send_finish(result); + if (!stream) { + throw new Error('No response stream received'); + } + + const dataStream = new Gio.DataInputStream({ + close_base_stream: true, + base_stream: stream + }); + + this.readResponse(dataStream, aiResponse); + } + catch (e) { + aiResponse.done = true; + aiResponse.addDelta('Error in vision API response: ' + e.message); + } + }); + } catch (error) { + throw error; } } } diff --git a/.config/hypr/hyprland/keybinds.conf b/.config/hypr/hyprland/keybinds.conf index 486bda1db..ec0ffba33 100644 --- a/.config/hypr/hyprland/keybinds.conf +++ b/.config/hypr/hyprland/keybinds.conf @@ -169,7 +169,7 @@ bindle=, XF86MonBrightnessUp, exec, ags run-js 'brightness.screen_value += 0.05; bindle=, XF86MonBrightnessDown, exec, ags run-js 'brightness.screen_value -= 0.05; indicator.popup(1);' # [hidden] bindl = , XF86AudioMute, exec, ags run-js 'indicator.popup(1);' # [hidden] bindl = Super+Shift,M, exec, ags run-js 'indicator.popup(1);' # [hidden] - +bindl= Ctrl,Print, exec, ags run-js 'sendScreenshotToGemini()' # Testing # bind = SuperAlt, f12, exec, notify-send "Hyprland version: $(hyprctl version | head -2 | tail -1 | cut -f2 -d ' ')" "owo" -a 'Hyprland keybind' # bind = Super+Alt, f12, exec, notify-send "Millis since epoch" "$(date +%s%N | cut -b1-13)" -a 'Hyprland keybind' From 36708f72cb7fcb3b8ecffcd531434f8615a1355c Mon Sep 17 00:00:00 2001 From: Abdullah Muhamed Date: Sat, 28 Dec 2024 16:49:04 +0200 Subject: [PATCH 2/3] =?UTF-8?q?alpha=20fedora=20support=F0=9F=AB=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .config/ags/modules/bar/layouts/index.js | 488 +++++++++++++++++++++++ README.md | 179 +++++---- install-fedora.sh | 403 +++++++++++++++++++ 3 files changed, 990 insertions(+), 80 deletions(-) create mode 100644 .config/ags/modules/bar/layouts/index.js create mode 100755 install-fedora.sh diff --git a/.config/ags/modules/bar/layouts/index.js b/.config/ags/modules/bar/layouts/index.js new file mode 100644 index 000000000..4c94761c9 --- /dev/null +++ b/.config/ags/modules/bar/layouts/index.js @@ -0,0 +1,488 @@ +// Layout definitions for different bar modes +import Widget from "resource:///com/github/Aylur/ags/widget.js"; +import ScrollableContainer from '../modules/scrollable.js'; +import App from 'resource:///com/github/Aylur/ags/app.js'; +import Variable from 'resource:///com/github/Aylur/ags/variable.js'; + +// Create variables to track sidebar visibility +const sideleftVisible = Variable(false); +const siderightVisible = Variable(false); + +App.connect('window-toggled', (_, name, visible) => { + if (name === 'sideleft') { + sideleftVisible.value = visible; + } + else if (name === 'sideright') { + siderightVisible.value = visible; + } +}); + +// Utility component for visual separation +export const Separator = () => Widget.Box({ + className: "txt-hugerass sec-txt", + css:"font-weight:100;", + child: Widget.Label({ label: "| " }), +}); + +export const Dent = () => Widget.Box({}); + +// Bar layout configurations for different modes +export const BarLayouts = { + 1:{ // Pads + name: 'Pads', + layout: (modules, monitor) => ({ + start: [ + Widget.Box({ + className: 'bar-round-padding', + children: [ + modules.StatusModules.battery(), + modules.workspaces.normal(), + ], + }), + Widget.Box({ + className: 'bar-round-padding', + children: [ + modules.ControlModules.shortcuts(), + ], + }) + ], + center: [Widget.Box({ + className: 'bar-round-padding', + children: [ + modules.InfoModules.clock(), + ], + })], + end: [ + Widget.Box({ + className: 'bar-round-padding', + children: [ + modules.InfoModules.weather(), + ], + }), + Widget.Box({ + children: [ + modules.StatusModules.tray(), + ], + }), + Widget.Box({ + className: 'bar-round-padding', + children: [ + modules.InfoModules.indicators(), + ], + }), + modules.ControlModules.button(), + ], + }), + }, + 2: { // Knocks with scrollable modules + name: 'Knocks', + layout: (modules) => ({ + start: [ + ], + center: [ + Widget.Revealer({ + transition: 'slide_right', + transitionDuration: 100, + connections: [[sideleftVisible, self => { + self.reveal_child = sideleftVisible.value; + }]], + child: ScrollableContainer({ + hpack: 'end', + sets: [ + [Widget.Box({ + className: 'bar-knocks padding-rl-15', + hexpand: true, + hpack: 'end', + children: [modules.InfoModules.colorPicker()], + })], + [Widget.Box({ + hexpand: true, + hpack: 'start', + className: 'bar-knocks padding-rl-15', + children: [ modules.InfoModules.colorscheme()], + })], + ], + }), + }), + ScrollableContainer({ + name: 'media', + sets: [ + [Widget.Box({ + hpack: 'fill', + hexpand: true, + className: 'bar-knocks padding-rl-5', + children: [modules.MediaModules.musicStuff()], + })], + [Widget.Box({ + hexpand: true, + hpack: 'fill', + className: 'bar-knocks padding-rl-15', + children: [ + Widget.Box({ hpack: 'center', children: [modules.AppModules.pinnedApps()]}) ], + })], + [Widget.Box({ + hpack: 'end', + hexpand: true, + className: 'bar-knocks', + children: [ Widget.Label({ label: " ",}) , modules.InfoModules.quote()], + })], + ], + }), + ScrollableContainer({ + name: 'media', + sets: [ + [Widget.Box({ + css:`min-width:16rem;`, + hexpand: true, + className: 'bar-knocks padding-rl-15', + children: [modules.workspaces.normal()], + })], + [Widget.Box({ + hexpand: true, + className: 'bar-knocks padding-rl-15', + children: [ + modules.InfoModules.weather() + ], + })], + ], + }), + + ScrollableContainer({ + name: 'tezy', + sets: [ + [Widget.Box({ + css:`min-width:20rem;`, + hexpand: true, + hpack: 'fill', + className: ' bar-knocks padding-rl-15', + children: [ + modules.InfoModules.simpleClock(), + modules.ControlModules.keyboard(), + modules.InfoModules.indicators(), + modules.StatusModules.resourcesBar(), + modules.StatusModules.battery(), + ] + })], + [Widget.Box({ + hpack: 'fill', + hexpand: true, + className: 'bar-knocks padding-rl-10', + children: [ Widget.Box({ hpack: 'center',hexpand: true, children: [modules.InfoModules.fetcher()]})], + })], + ], + }), + Widget.Revealer({ + transition: 'slide_left', + transitionDuration: 120, + connections: [[siderightVisible, self => { + self.reveal_child = siderightVisible.value; + }]], + child: ScrollableContainer({ + sets: [ + [Widget.Box({ + hpack: 'center', + css:`min-width:18rem;`, + className: 'spacing-h-15 bar-knocks padding-rl-15', + children: [ modules.ControlModules.shortcuts()], + })], + [Widget.Box({ + css:`min-width:23rem;`, + className: 'spacing-h-15 bar-knocks padding-rl-15', + children: [ modules.InfoModules.logo() , modules.InfoModules.quote()], + })], + ], + }), + }), + + + + // Second scrollable for media + + ], + end: [ + ], + }), + }, + 3: { // Normal + name: 'normal', + className: 'bar-bg', + css:"min-height:2.23rem", + layout: (modules) => ({ + start: [ + modules.InfoModules.windowTitle(), + ], + center: [ + modules.MediaModules.music(), + modules.workspaces.normal(), + modules.StatusModules.system(), + ], + end: [ + modules.InfoModules.indicators(), + ], + }), + }, + 4: { // Minimal + name: 'Minimal', + className: 'bar-floating-short', + css:"min-height:2.71rem", + layout: (modules) => ({ + start: [ ScrollableContainer({ + name: 'media', + sets: [ + [Widget.Box({ + hexpand: true, + halign: 'center', + children: [ + modules.StatusModules.battery(), + ], + })], + [Widget.Box({})], + ], + }), + ScrollableContainer({ + name: 'media', + sets: [ + [Widget.Box({ + hexpand: true, + halign: 'center', + children: [ + modules.workspaces.normal(), + ], + })], + [Widget.Box({ + hexpand: true, + hpack: 'center', + children: [modules.workspaces.focus()], + })], + [Widget.Box({ + hexpand: true, + hpack: 'center', + children: [modules.ControlModules.shortcuts()], + })], + ], + }), + ], + center: [ + ScrollableContainer({ + name: 'media', + sets: [ + [Widget.Box({ + children: [ + modules.InfoModules.weather(), + ], + })], + [Widget.Box({ + hpack:"center", + hexpand:true, + children: [modules.InfoModules.clock()], + })], + [Widget.Box({ + children: [modules.AppModules.pinnedApps()], + })], + [Widget.Box({ + spacing:10, + hpack:"center", + hexpand:true, + // className: "spacing-h-5", + children: [modules.InfoModules.colorPicker()], + })], + Widget.Box({}), + ], + }), + ], + end: [ + ScrollableContainer({ + name: 'media', + sets: [ + [Widget.Box({ + children: [modules.StatusModules.tray()], + })], + [Widget.Box({ + children: [], + })], + ], + }), + ScrollableContainer({ + name: 'media', + sets: [ + [Widget.Box({ + children: [modules.InfoModules.indicators()], + })], + [Widget.Box({ + children: [], + })], + ], + }), + ], + }), + }, +}; + +// Create bar content with proper layout and error handling +export const createBarContent = async (layout, modules, monitor) => { + try { + const config = await layout.layout(modules, monitor); + + // Helper function to safely create a widget + const createWidget = (factory) => { + try { + if (typeof factory === 'function') { + return factory(); + } + return factory; + } catch (error) { + console.error('Error creating widget:', error); + return null; + } + }; + + // Helper function to safely add widgets to a container + const addWidgets = (container, widgets = []) => { + const children = []; + widgets.filter(Boolean).forEach(widget => { + const newWidget = createWidget(widget); + if (newWidget) { + // If widget has a parent, create a new instance instead + if (newWidget.parent) { + try { + const clone = newWidget.constructor(); + if (clone.setup) { + clone.setup(clone); + } + children.push(clone); + } catch (error) { + console.error('Error cloning widget:', error); + } + } else { + children.push(newWidget); + } + } + }); + + // Remove existing children first + container.children = []; + + // Then add new children + container.children = children; + return children; + }; + + // Create boxes for each section + const startBox = Widget.Box({ + className: 'bar-start spacing-h-5', + hpack: 'start', + setup: self => { + self.connect('destroy', () => { + (self.children || []).forEach(child => { + if (child?.destroy) { + try { + child.destroy(); + } catch (error) { + console.error('Error destroying child:', error); + } + } + }); + }); + }, + }); + + const centerBox = Widget.Box({ + className: 'bar-center spacing-h-5', + hpack: 'center', + setup: self => { + self.connect('destroy', () => { + (self.children || []).forEach(child => { + if (child?.destroy) { + try { + child.destroy(); + } catch (error) { + console.error('Error destroying child:', error); + } + } + }); + }); + }, + }); + + const endBox = Widget.Box({ + className: 'bar-end spacing-h-5', + hpack: 'end', + setup: self => { + self.connect('destroy', () => { + (self.children || []).forEach(child => { + if (child?.destroy) { + try { + child.destroy(); + } catch (error) { + console.error('Error destroying child:', error); + } + } + }); + }); + }, + }); + + // Add widgets to each section + const startWidgets = []; + const centerWidgets = []; + const endWidgets = []; + + // Add corner modules if needed + const corners = layout.corners || { + topLeft: false, + topRight: false, + bottomLeft: false, + bottomRight: false, + }; + + if (corners.topLeft && modules.CornerModules?.topleft) { + const corner = createWidget(modules.CornerModules.topleft); + if (corner) startWidgets.push(corner); + } + + // Add regular widgets + if (config.start) startWidgets.push(...config.start); + if (config.center) centerWidgets.push(...config.center); + if (config.end) endWidgets.push(...config.end); + + if (corners.topRight && modules.CornerModules?.topright) { + const corner = createWidget(modules.CornerModules.topright); + if (corner) endWidgets.push(corner); + } + + // Add widgets to containers + const addedStart = addWidgets(startBox, startWidgets); + const addedCenter = addWidgets(centerBox, centerWidgets); + const addedEnd = addWidgets(endBox, endWidgets); + + const content = Widget.Box({ + className: layout.className || '', + css: layout.css || '', + child: Widget.CenterBox({ + className: 'bar-content', + startWidget: startBox, + centerWidget: centerBox, + endWidget: endBox, + }), + setup: self => { + self.connect('destroy', () => { + // Clean up all widgets + [...addedStart, ...addedCenter, ...addedEnd].forEach(widget => { + if (widget?.destroy) { + try { + widget.destroy(); + } catch (error) { + console.error('Error destroying widget:', error); + } + } + }); + }); + }, + }); + + return content; + } catch (error) { + console.error('Error creating bar content:', error); + return null; + } +}; \ No newline at end of file diff --git a/README.md b/README.md index 7c9945ff8..e1938f327 100644 --- a/README.md +++ b/README.md @@ -1,116 +1,135 @@
-

【 SH1ZICUS HYPRLAND DOTFILES 】

+

【 Pharmaracism's Hyprland Rice 】

- +

-![](https://img.shields.io/github/last-commit/sh1zicus/dots-hyprland?&style=for-the-badge&color=FFB1C8&logoColor=D9E0EE&labelColor=292324) -[![](https://img.shields.io/github/repo-size/sh1zicus/dots-hyprland?color=CAC992&label=SIZE&logo=googledrive&style=for-the-badge&logoColor=D9E0EE&labelColor=292324)](https://github.com/sh1zicus/dots-hyprland) -![](https://img.shields.io/github/stars/sh1zicus/dots-hyprland?style=for-the-badge&logo=andela&color=FFB686&logoColor=D9E0EE&labelColor=292324) +[![GitHub stars](https://img.shields.io/github/stars/pharmaracist/dots-hyprland?style=for-the-badge&logo=github&color=FFB686&logoColor=D9E0EE&labelColor=292324)](https://github.com/pharmaracist/dots-hyprland/stargazers) +[![GitHub last commit](https://img.shields.io/github/last-commit/pharmaracist/dots-hyprland?style=for-the-badge&logo=github&color=FFB1C8&logoColor=D9E0EE&labelColor=292324)](https://github.com/pharmaracist/dots-hyprland/commits/main) +[![Repo size](https://img.shields.io/github/repo-size/pharmaracist/dots-hyprland?style=for-the-badge&logo=github&color=CAC992&logoColor=D9E0EE&labelColor=292324)](https://github.com/pharmaracist/dots-hyprland) +[![License](https://img.shields.io/github/license/pharmaracist/dots-hyprland?style=for-the-badge&logo=github&color=8CABD9&logoColor=D9E0EE&labelColor=292324)](https://github.com/pharmaracist/dots-hyprland/blob/main/LICENSE) +[![GitHub issues](https://img.shields.io/github/issues/pharmaracist/dots-hyprland?style=for-the-badge&logo=github&color=F5A97F&logoColor=D9E0EE&labelColor=292324)](https://github.com/pharmaracist/dots-hyprland/issues) +[![Required Arch](https://img.shields.io/badge/Supports-Arch%20Linux-blue?style=for-the-badge&logo=arch-linux&logoColor=D9E0EE&labelColor=292324&color=7DC4E4)](https://archlinux.org/) +[![Required Fedora](https://img.shields.io/badge/Supports-Fedora-blue?style=for-the-badge&logo=fedora&logoColor=D9E0EE&labelColor=292324&color=51A2DA)](https://getfedora.org/) +[![Hyprland](https://img.shields.io/badge/Made%20for-Hyprland-pink?style=for-the-badge&logo=linux&logoColor=D9E0EE&labelColor=292324&color=C6A0F6)](https://hyprland.org/) + + + Join our Discord
-

• overview •

-

+

🌟 A Modern, Beautiful & Feature-rich Hyprland Configuration 🌟

- -
- Notable features - - - **Overview widget**: shows open apps. Type to search/calculate/run - - **AI Assistant**: ChatGPT and Google Gemini - - **Autogenerated colors** based on your wallpaper using [Material colors](https://m3.material.io/styles/color/the-color-system/key-colors-tones) - - **Animations** that are natural and fluid - - **Transparent installation**: every command is shown before it's run -
-
- Instructions - - - **Automatic**, but guided and transparent, installation for Arch(-based) Linux: - ```bash - bash <(curl -s "https://sh1zicus.github.io/dots-hyprland-wiki/setup.sh") - ``` - - **Manual** installation, other distros and more: - - See the [Wiki](https://sh1zicus.github.io/dots-hyprland-wiki/en/i-i/01setup/) - - (_Available in: English, Vietnamese, and Simplified Chinese. Translations are welcome._) - - - **Default keybinds**: Parts similar to Windows and GNOME. Hit Super+/ for a list. -
- Here's an image, just in case: - - ![image](https://github.com/sh1zicus/dots-hyprland/assets/97237370/4c3d27b4-9ac5-4e55-9cae-c5c1f497890f) - -
- +

+ + + +

+ +
+✨ Features + +### 🎮 Core Features +- **Smart Desktop**: Overview widget, AI assistants (ChatGPT/Gemini), dynamic workspaces +- **Beautiful UI**: Material You design, fluid animations, custom widgets +- **Rich Media**: Spotify with Spicetify, MPV, MPD, YouTube Music +- **Productivity**: Todo list, calendar, timers, multi-monitor support +- **Development**: Full dev environment with VSCode, Git, Python setup +- **System Tools**: Resource monitoring, package management, auto-updates +
- Software overview +🚀 Installation +### 🐧 For Arch Linux Users: +```bash +curl -s https://raw.githubusercontent.com/pharmaracist/dots-hyprland/main/install.sh | bash +``` - | Software | Purpose | - | ------------- | ------------- | - | [Hyprland](https://github.com/hyprwm/hyprland) | The compositor (for noobs, you can just call it a window manager) | - | [AGS](https://github.com/Aylur/ags) | A GTK widget system, responsible for the status bar, sidebars, etc. | - | [Fuzzel](https://mark.stosberg.com/fuzzel-a-great-dmenu-and-rofi-altenrative-for-wayland/) | For clipboard and emoji picker | +### 🎩 For Fedora Users: +```bash +curl -s https://raw.githubusercontent.com/pharmaracist/dots-hyprland/main/install-fedora.sh | bash +``` +### 💻 Manual Installation: +If you prefer to see what's being installed, you can manually clone and run the installation script: - - For a more comprehensive list of dependencies, see [scriptdata/dependencies.conf](https://github.com//dots-hyprland/blob/main/scriptdata/dependencies.conf) -
+```bash +# Clone the repository +git clone https://github.com/pharmaracist/dots-hyprland.git -
- Help improve these dotfiles! - - - Join the [discussions](https://github.com/sh1zicus/dots-hyprland/discussions) - - If you'd like to suggest fixes or a new widget, feel free to [open an issue](https://github.com/sh1zicus/dots-hyprland/issues/new/choose) -
+# Change to the directory +cd dots-hyprland -
-

• screenshots •

-

-
+# For Arch Linux +chmod +x install.sh +./install.sh -### Main branch (*illogical-impulse*) +# For Fedora +chmod +x install-fedora.sh +./install-fedora.sh +``` -**AI, on-screen indicators** -![image](https://github.com/sh1zicus/dots-hyprland/assets/97237370/5e081770-0f1e-45c4-ad9c-3d19f488cd85) +### 🔧 System Requirements -**Fancy notifications, music controls, system, calendar** -![image](https://github.com/sh1zicus/dots-hyprland/assets/97237370/406b72b6-fa38-4f0d-a6c4-4d7d5d5ddcb7) +#### For Arch Linux: +- Base Arch Linux installation +- Internet connection +- Basic terminal knowledge -**Easy window management** -![image](https://github.com/sh1zicus/dots-hyprland/assets/97237370/14e9725c-789f-4412-87b6-cce9504db109) +#### For Fedora: +- Fedora Workstation 38 or newer +- Internet connection +- Basic terminal knowledge -
-

• thank you •

-

-
+### 🎨 Features +
- - [@end-4](https://github.com/end-4) for the base config - - [@clsty](https://github.com/clsty) for making an actually good install script + many other stuff that I neglect - - [@midn8hustlr](https://github.com/midn8hustlr) for greatly improving the color generation system - - AGS: [Aylur's config](https://github.com/Aylur/dotfiles), [kotontrion's config](https://github.com/kotontrion/dotfiles) - - EWW: [fufexan's config](https://github.com/fufexan/dotfiles) (he thanks more people there btw) - - AI bots for providing useful examples - - Open source contributors for their software and ricers for their insipration (would be a too long list to put here!) +
+📋 Requirements -
-

• stonks •

-

-
+- Arch Linux or Arch-based distro +- X86_64 architecture +- Internet connection +- Basic terminal knowledge +- A Brain 🙂🧠 +
-[![Stargazers over time](https://starchart.cc/sh1zicus/dots-hyprland.svg?background=%230d1117&axis=%23e6edf3&line=%234759e7)](https://starchart.cc/sh1zicus/dots-hyprland) +## 🖼️ Gallery
-

• inspirations •

-

+
+ 📸 Screenshots +
+ +
+ Screenshot 1 + Screenshot 2 + Screenshot 3 + Screenshot 4 + Screenshot 4 + Screenshot 5 + Screenshot 6 + Screenshot 6 + +
+ +
- - osu!lazer, Windows 11, Material Design 3, AvdanOS (concept) +## 🙏 Credits +- End-4 for the original Hyprland rice +- Sh1zicus for the help to make this work and the awesome kickstart +- My GF for the support hehe !! +- Hyprland community +- All the amazing ricers at Unixporn +- osu!lazer, Novelknocks , Hybrid , AvdanOS (concept), Old End4 Configs +*Note: This README was made by AI cuz im too lazy to write it myself! ..Pharmaracist* diff --git a/install-fedora.sh b/install-fedora.sh new file mode 100755 index 000000000..926e2f32c --- /dev/null +++ b/install-fedora.sh @@ -0,0 +1,403 @@ +#!/usr/bin/env bash + +# ANSI color codes +MAGENTA='\e[35m' +BLUE='\e[34m' +GREEN='\e[32m' +RED='\e[31m' +CYAN='\e[36m' +YELLOW='\e[33m' +RESET='\e[0m' + +clear +echo -e "${MAGENTA}" +echo ' +██████╗ ██╗ ██╗ █████╗ ██████╗ ███╗ ███╗ █████╗ ██████╗ █████╗ ██████╗██╗███████╗████████╗ +██╔══██╗██║ ██║██╔══██╗██╔══██╗████╗ ████║██╔══██╗██╔══██╗██╔══██╗██╔════╝██║██╔════╝╚══██╔══╝ +██████╔╝███████║███████║██████╔╝██╔████╔██║███████║██████╔╝███████║██║ ██║███████╗ ██║ +██╔═══╝ ██╔══██║██╔══██║██╔══██╗██║╚██╔╝██║██╔══██║██╔══██╗██╔══██║██║ ██║╚════██║ ██║ +██║ ██║ ██║██║ ██║██║ ██║██║ ╚═╝ ██║██║ ██║██║ ██║██║ ██║╚██████╗██║███████║ ██║ +╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝╚══════╝ ╚═╝ +' +echo -e "${GREEN}=====================================================================${RESET}" +echo -e "${BLUE} 🚀 Welcome to the Installation! 🚀 ${RESET}" +echo -e "${YELLOW} Enhanced by Pharmaracist ${RESET}" +echo -e "${YELLOW} Fedora Edition 🎩 ${RESET}" +echo -e "${GREEN}=====================================================================${RESET}" + +# Fun loading messages +MESSAGES=( + "🕌 Configuring Islamic prayer times and Quran features..." + "🎨 Generating beautiful color schemes with pywal..." + "🎵 Setting up Spotify theme integration..." + "⚡ Optimizing system performance..." + "🌙 Adding Hijri calendar support..." + "🎉 Making things awesome..." + "🚀 Preparing for launch..." + "🌈 Adding some colors to your life..." + "✨ Sprinkling some magic..." + "🔧 Tightening the nuts and bolts..." +) + +show_message() { + echo -e "\n${CYAN}${MESSAGES[$((RANDOM % ${#MESSAGES[@]}))]}\n${RESET}" +} + +cd "$(dirname "$0")" +export base="$(pwd)" + +show_message +source ./scriptdata/environment-variables +source ./scriptdata/functions +source ./scriptdata/fedora-installers +source ./scriptdata/options + +# Add log file setup at the beginning +LOG_FILE="./installation_$(date +%Y%m%d_%H%M%S).log" +exec 1> >(tee -a "$LOG_FILE") 2>&1 + +echo -e "${CYAN}🕒 Installation started at $(date)${RESET}" +echo -e "${BLUE}💻 System information:${RESET}" +uname -a + +show_message + +##################################################################################### +if ! command -v dnf >/dev/null 2>&1; then + printf "${RED}[$0]: 🚫 Oops! This script needs dnf (Fedora). Are you on the right system? 🤔${RESET}\n" + exit 1 +fi +prevent_sudo_or_root + +echo -e "${BLUE}Setting up sudo permissions...${RESET}" +echo -e "${YELLOW}We'll ask for your password once to avoid multiple prompts${RESET}" + +# Create temporary sudoers file for our commands +echo -e "${CYAN}Creating temporary sudo rules...${RESET}" +SUDOERS_FILE="/etc/sudoers.d/illogical-impulse-temp" +sudo tee "$SUDOERS_FILE" > /dev/null << EOL +# Temporary sudo rules for illogical-impulse installation +$(whoami) ALL=(ALL) NOPASSWD: /usr/bin/dnf +$(whoami) ALL=(ALL) NOPASSWD: /usr/bin/chmod +$(whoami) ALL=(ALL) NOPASSWD: /usr/bin/cp +$(whoami) ALL=(ALL) NOPASSWD: /usr/bin/mkdir +$(whoami) ALL=(ALL) NOPASSWD: /usr/bin/tee +$(whoami) ALL=(ALL) NOPASSWD: /usr/bin/usermod +$(whoami) ALL=(ALL) NOPASSWD: /usr/bin/gpasswd +EOL + +# Make sure the file is secure +sudo chmod 440 "$SUDOERS_FILE" + +# Function to clean up sudo rules on script exit +cleanup_sudo() { + echo -e "${YELLOW}Cleaning up temporary sudo rules...${RESET}" + sudo rm -f "$SUDOERS_FILE" +} + +# Register cleanup function to run on script exit +trap cleanup_sudo EXIT + +# Keep sudo alive throughout the script +while true; do + sudo -n true + sleep 60 + kill -0 "$$" || exit +done 2>/dev/null & + +startask () { + printf "${BLUE}[$0]: 👋 Hi there! Before we start:\n" + printf "${YELLOW}This script requires:\n" + printf " 1. 🐧 Fedora Linux\n" + printf " 2. 💻 Basic terminal knowledge\n" + printf " 3. 🧠 A functioning brain (very important!)\n${RESET}" + + printf "${MAGENTA}\nWould you like to create a backup? (recommended) [y/N]: ${RESET}" + read -p " " backup_confirm + case $backup_confirm in + [yY][eE][sS]|[yY]) + echo -e "${GREEN}Smart choice! Backing up your configs... 📦${RESET}" + backup_configs + ;; + *) echo -e "${YELLOW}Living dangerously, I see! Skipping backup... 🎲${RESET}" + ;; + esac + + printf '\n' + printf "${CYAN}Do you want to confirm every command before execution?\n" + printf " y = Yes, I want to see everything (DEFAULT)\n" + printf " n = No, I trust you (YOLO mode 🎢 - we'll only ask for sudo once)\n" + printf " a = Actually, let me out of here! 🚪${RESET}\n" + read -p "Choose wisely: " p + case $p in + n) + echo -e "${YELLOW}YOLO mode activated! 🎢${RESET}" + echo -e "${CYAN}Don't worry about passwords - we'll handle sudo for you 🔐${RESET}" + ask=false + ;; + a) + echo -e "${RED}See you next time! 👋${RESET}" + exit 1 + ;; + *) + echo -e "${GREEN}Playing it safe - good choice! 🛡️${RESET}" + ask=true + ;; + esac +} + +case $ask in + false)sleep 0 ;; + *)startask ;; +esac + +set -e +##################################################################################### +printf "${CYAN}[$0]: 1. Setting up RPM Fusion repositories and core packages\n${RESET}" + +# Enable RPM Fusion repositories +if ! dnf repolist | grep -q "rpmfusion-free" || ! dnf repolist | grep -q "rpmfusion-nonfree"; then + echo -e "${YELLOW}Setting up RPM Fusion repositories...${RESET}" + v sudo dnf install -y https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm + v sudo dnf install -y https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm + v sudo dnf groupupdate -y core +fi + +# System update +case $SKIP_SYSUPDATE in + true) sleep 0;; + *) + echo -e "${YELLOW}Updating system packages...${RESET}" + v sudo dnf upgrade --refresh -y + v sudo dnf groupupdate -y multimedia --setop="install_weak_deps=False" --exclude=PackageKit-gstreamer-plugin + v sudo dnf groupupdate -y sound-and-video + ;; +esac + +# Install Development Tools and Libraries +echo -e "${YELLOW}Installing development tools and libraries...${RESET}" +v sudo dnf group install -y "Development Tools" "Development Libraries" +v sudo dnf install -y cmake gcc-c++ libxcb-devel libX11-devel pixman-devel cairo-devel pango-devel + +# Install Wayland Development Packages +echo -e "${YELLOW}Installing Wayland development packages...${RESET}" +v sudo dnf install -y wayland-devel libwayland-client libwayland-cursor libwayland-egl wayland-protocols-devel + +# Install XDG Desktop Portal +echo -e "${YELLOW}Installing XDG desktop portal and dependencies...${RESET}" +v sudo dnf install -y xdg-desktop-portal xdg-desktop-portal-gtk xdg-desktop-portal-wlr + +# Install COPR for Hyprland +if ! dnf repolist | grep -q "solopasha-hyprland"; then + echo -e "${YELLOW}Adding Hyprland COPR repository...${RESET}" + v sudo dnf copr enable -y solopasha/hyprland + v sudo dnf copr enable -y erikreider/SwayNotificationCenter +fi + +# Install Hyprland and related packages +echo -e "${YELLOW}Installing Hyprland and related packages...${RESET}" +v sudo dnf install -y hyprland waybar wofi wlroots swww grim slurp wl-clipboard swaylock swayidle swaybg + +# Install Additional Dependencies +echo -e "${YELLOW}Installing additional required packages...${RESET}" +v sudo dnf install -y \ + python3-pip \ + python3-gobject \ + gtk3 \ + gtk4 \ + polkit-gnome \ + network-manager-applet \ + brightnessctl \ + playerctl \ + pamixer \ + pavucontrol \ + blueman \ + NetworkManager-tui \ + xdg-user-dirs \ + xdg-utils \ + qt5-qtwayland \ + qt6-qtwayland \ + ripgrep \ + jq \ + ImageMagick \ + nodejs \ + npm \ + neofetch + +# Install fonts +echo -e "${YELLOW}Installing required fonts...${RESET}" +v sudo dnf install -y \ + google-noto-fonts-common \ + google-noto-sans-fonts \ + google-noto-serif-fonts \ + jetbrains-mono-fonts \ + fontawesome-fonts \ + liberation-fonts \ + mozilla-fira-*-fonts + +# Read dependencies from configuration +remove_bashcomments_emptylines ${DEPLISTFILE} ./cache/dependencies_stripped.conf +readarray -t pkglist < ./cache/dependencies_stripped.conf + +# Install core packages +if (( ${#pkglist[@]} != 0 )); then + if $ask; then + for i in "${pkglist[@]}"; do + v sudo dnf install -y $i + done + else + v sudo dnf install -y ${pkglist[*]} + fi +fi + +# Theme integration packages +install_theme_integrations() { + if $ask; then + echo -e "${YELLOW}[$0]: Would you like to install theme integration for Discord? [y/N]\n${RESET}" + read -p "====> " discord + case $discord in + [yY]) + v pip install --user pywal-discord-git + echo -e "${GREEN}Discord theme integration installed!${RESET}" + echo -e "${YELLOW}Note: You'll need to restart Discord after installation${RESET}" + ;; + *) echo "Skipping Discord theme integration" ;; + esac + + echo -e "${YELLOW}[$0]: Would you like to install theme integration for Firefox? [y/N]\n${RESET}" + read -p "====> " firefox + case $firefox in + [yY]) + v pip install --user pywalfox + echo -e "${GREEN}Firefox theme integration installed!${RESET}" + echo -e "${YELLOW}Note: You'll need to install the Pywalfox extension from Firefox Add-ons${RESET}" + echo -e "${YELLOW}Visit: https://addons.mozilla.org/en-US/firefox/addon/pywalfox/${RESET}" + ;; + *) echo "Skipping Firefox theme integration" ;; + esac + + echo -e "${YELLOW}[$0]: Would you like to install theme integration for Spotify? [y/N]\n${RESET}" + read -p "====> " spotify + case $spotify in + [yY]) + # Install Spotify from RPM Fusion if not already installed + if ! command -v spotify &> /dev/null; then + echo -e "${YELLOW}Installing Spotify from RPM Fusion...${RESET}" + v sudo dnf install -y spotify-client + fi + + # Install Spicetify + echo -e "${YELLOW}Installing Spicetify...${RESET}" + v curl -fsSL https://raw.githubusercontent.com/spicetify/spicetify-cli/master/install.sh | sh + + # Install Pywal for Spicetify + v pip install --user python-pywal-spicetify-git + + echo -e "${GREEN}Spotify theme integration installed!${RESET}" + echo -e "${YELLOW}Note: You'll need to restart Spotify after installation${RESET}" + ;; + *) echo "Skipping Spotify theme integration" ;; + esac + else + # Non-interactive installation + echo -e "${YELLOW}Installing all theme integrations...${RESET}" + v pip install --user pywal-discord-git pywalfox python-pywal-spicetify-git + + # Install Spotify and Spicetify + if ! command -v spotify &> /dev/null; then + v sudo dnf install -y spotify-client + fi + v curl -fsSL https://raw.githubusercontent.com/spicetify/spicetify-cli/master/install.sh | sh + + echo -e "${GREEN}All theme integrations installed!${RESET}" + echo -e "${YELLOW}Notes:${RESET}" + echo -e "${YELLOW}- Restart Discord and Spotify after installation${RESET}" + echo -e "${YELLOW}- Install the Pywalfox extension from: https://addons.mozilla.org/en-US/firefox/addon/pywalfox/${RESET}" + fi +} + +v install_theme_integrations + +v sudo -n usermod -aG video,i2c,input "$(whoami)" +v bash -c "echo i2c-dev | sudo -n tee /etc/modules-load.d/i2c-dev.conf" +v systemctl --user enable ydotool --now +v gsettings set org.gnome.desktop.interface font-name 'Rubik 11' + +# Install custom fonts +echo -e "${BLUE}[$0]: Installing custom fonts... 🔤${RESET}" +if [ -d "$DOTS_DIR/.fonts" ] && [ "$(ls -A $DOTS_DIR/.fonts)" ]; then + mkdir -p "$HOME/.local/share/fonts" + if cp -r "$DOTS_DIR/.fonts/"* "$HOME/.local/share/fonts/"; then + fc-cache -f + echo -e "${GREEN}[$0]: Custom fonts installed successfully! ✨${RESET}" + else + echo -e "${RED}[$0]: Error copying fonts to local directory${RESET}" + fi +else + echo -e "${YELLOW}[$0]: No custom fonts found in .fonts directory${RESET}" +fi + +# Set up Spotify permissions and customization +echo -e "${BLUE}[$0]: Setting up Spotify and Spicetify... 🎵${RESET}" +v sudo -n chmod a+wr /opt/spotify +v sudo -n chmod a+wr /opt/spotify/Apps -R + +# Run spicetify script +echo -e "${CYAN}Running Spicetify customization script...${RESET}" +v chmod +x ./scriptdata/spicetify.sh +v ./scriptdata/spicetify.sh + +show_message + +echo -e "${GREEN}=====================================================================${RESET}" +echo -e "${BLUE} ☪️ Islamic Features Setup ${RESET}" +echo -e "${GREEN}=====================================================================${RESET}" +echo -e "${CYAN}Setting up Islamic features:${RESET}" +echo -e "${YELLOW}1. 🕌 Prayer times integration${RESET}" +echo -e "${YELLOW}2. 📖 Quran reader and references${RESET}" + +echo -e "${GREEN}=====================================================================${RESET}" +echo -e "${BLUE} 🎨 Color Harmony Setup ${RESET}" +echo -e "${GREEN}=====================================================================${RESET}" +echo -e "${CYAN}Configuring pywal integration:${RESET}" +echo -e "${YELLOW}1. 🖼️ Dynamic color generation from wallpapers${RESET}" +echo -e "${YELLOW}2. 🎵 Spotify theme synchronization${RESET}" +echo -e "${YELLOW}3. 📝 Terminal and application theme matching${RESET}" +echo -e "${YELLOW}4. 🔄 Real-time color updates${RESET}" + +##################################################################################### +printf "${CYAN}[$0]: Finished. See the \"Import Manually\" folder and grab anything you need.\n${RESET}" +printf "\n" +printf "${CYAN}If you are new to Hyprland, please read\n" +printf "https://sh1zicus.github.io/dots-hyprland-wiki/en/i-i/01setup/#post-installation\n" +printf "for hints on launching Hyprland.\n${RESET}" +printf "\n" + +echo -e "${GREEN}=====================================================================${RESET}" +echo -e "${MAGENTA} 🎉 Installation Complete! 🎉 ${RESET}" +echo -e "${GREEN}=====================================================================${RESET}" +echo -e "${CYAN}What's next?${RESET}" +echo -e "${YELLOW}1. 🔄 Log out and log back in to apply all changes${RESET}" +echo -e "${YELLOW}2. 🕌 Check your prayer times widget (Alt+P)${RESET}" +echo -e "${YELLOW}3. 📖 Open the Quran reader (Alt+Q)${RESET}" +echo -e "${YELLOW}4. 🎨 Try changing wallpapers to see pywal in action${RESET}" +echo -e "${YELLOW}5. 🌙 Configure your local prayer times in the settings${RESET}" +echo -e "${YELLOW}6. ⭐ Don't forget to star the repo if you like it!${RESET}" + +echo -e "\n${BLUE}Useful Keyboard Shortcuts:${RESET}" +echo -e "${CYAN}Alt + P${RESET} → ${YELLOW}Prayer Times Widget${RESET}" +echo -e "${CYAN}Alt + Q${RESET} → ${YELLOW}Quran Reader${RESET}" +echo -e "${CYAN}Alt + W${RESET} → ${YELLOW}Change Wallpaper (auto-updates themes)${RESET}" + +echo -e "\n${BLUE}Need help? Check out:${RESET}" +echo -e "${CYAN}https://sh1zicus.github.io/dots-hyprland-wiki/en/i-i/01setup/#post-installation${RESET}" +echo -e "\n${GREEN}May Allah bless your journey! 🌙${RESET}\n" + +case $existed_ags_opt in + y) printf "\n${YELLOW}[$0]: Warning: \"$XDG_CONFIG_HOME/ags/user_options.js\" already existed before and we didn't overwrite it. \n${RESET}";; +esac +case $existed_hypr_conf in + y) printf "\n${YELLOW}[$0]: Warning: \"$XDG_CONFIG_HOME/hypr/hyprland.conf\" already existed before and we didn't overwrite it. \n${RESET}" + printf "${YELLOW}Please use \"$XDG_CONFIG_HOME/hypr/hyprland.conf.new\" as a reference for a proper format.\n${RESET}" + printf "${YELLOW}If this is your first time installation, you must overwrite \"$XDG_CONFIG_HOME/hypr/hyprland.conf\" with \"$XDG_CONFIG_HOME/hypr/hyprland.conf.new\".\n${RESET}" +;;esac From 47b925b9b224cf65f2deb57af19af44d4212c962 Mon Sep 17 00:00:00 2001 From: Abdullah Muhamed Date: Sat, 28 Dec 2024 16:55:08 +0200 Subject: [PATCH 3/3] Update install-fedora.sh --- install-fedora.sh | 50 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/install-fedora.sh b/install-fedora.sh index 926e2f32c..685a73500 100755 --- a/install-fedora.sh +++ b/install-fedora.sh @@ -174,18 +174,31 @@ case $SKIP_SYSUPDATE in ;; esac +# Enable Flathub if not already enabled +if ! flatpak remotes --show-details | grep -q "Flathub"; then + echo -e "${YELLOW}Enabling Flathub repository...${RESET}" + v flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo +fi + # Install Development Tools and Libraries echo -e "${YELLOW}Installing development tools and libraries...${RESET}" v sudo dnf group install -y "Development Tools" "Development Libraries" -v sudo dnf install -y cmake gcc-c++ libxcb-devel libX11-devel pixman-devel cairo-devel pango-devel +v sudo dnf install -y cmake gcc-c++ libxcb-devel libX11-devel pixman-devel cairo-devel \ + pango-devel wayland-devel wayland-protocols-devel libdrm-devel \ + libxkbcommon-devel systemd-devel libinput-devel libevdev-devel \ + mesa-libEGL-devel mesa-libGLES-devel mesa-libgbm-devel \ + xcb-util-devel xcb-util-keysyms-devel xcb-util-cursor-devel \ + xcb-util-wm-devel xcb-util-renderutil-devel # Install Wayland Development Packages echo -e "${YELLOW}Installing Wayland development packages...${RESET}" -v sudo dnf install -y wayland-devel libwayland-client libwayland-cursor libwayland-egl wayland-protocols-devel +v sudo dnf install -y wayland-devel libwayland-client libwayland-cursor \ + libwayland-egl wayland-protocols-devel # Install XDG Desktop Portal echo -e "${YELLOW}Installing XDG desktop portal and dependencies...${RESET}" -v sudo dnf install -y xdg-desktop-portal xdg-desktop-portal-gtk xdg-desktop-portal-wlr +v sudo dnf install -y xdg-desktop-portal xdg-desktop-portal-gtk \ + xdg-desktop-portal-wlr xdg-user-dirs xdg-utils # Install COPR for Hyprland if ! dnf repolist | grep -q "solopasha-hyprland"; then @@ -196,7 +209,9 @@ fi # Install Hyprland and related packages echo -e "${YELLOW}Installing Hyprland and related packages...${RESET}" -v sudo dnf install -y hyprland waybar wofi wlroots swww grim slurp wl-clipboard swaylock swayidle swaybg +v sudo dnf install -y hyprland waybar wofi wlroots swww grim slurp \ + wl-clipboard swaylock swayidle swaybg dunst rofi-wayland \ + kitty alacritty foot wezterm # Install Additional Dependencies echo -e "${YELLOW}Installing additional required packages...${RESET}" @@ -213,8 +228,6 @@ v sudo dnf install -y \ pavucontrol \ blueman \ NetworkManager-tui \ - xdg-user-dirs \ - xdg-utils \ qt5-qtwayland \ qt6-qtwayland \ ripgrep \ @@ -222,7 +235,17 @@ v sudo dnf install -y \ ImageMagick \ nodejs \ npm \ - neofetch + neofetch \ + eww \ + socat \ + cpio \ + unzip \ + sassc + +# Install additional multimedia codecs +echo -e "${YELLOW}Installing multimedia codecs...${RESET}" +v sudo dnf install -y gstreamer1-plugins-{bad-\*,good-\*,base} \ + gstreamer1-plugin-openh264 mozilla-openh264 # Install fonts echo -e "${YELLOW}Installing required fonts...${RESET}" @@ -233,7 +256,18 @@ v sudo dnf install -y \ jetbrains-mono-fonts \ fontawesome-fonts \ liberation-fonts \ - mozilla-fira-*-fonts + mozilla-fira-*-fonts \ + fira-code-fonts + +# Install Flatpak applications +echo -e "${YELLOW}Installing Flatpak applications...${RESET}" +v flatpak install -y flathub com.github.tchx84.Flatseal + +# Enable services +echo -e "${YELLOW}Enabling required services...${RESET}" +v systemctl --user enable --now wireplumber.service +v systemctl --user enable --now pipewire.service +v systemctl --user enable --now pipewire-pulse.service # Read dependencies from configuration remove_bashcomments_emptylines ${DEPLISTFILE} ./cache/dependencies_stripped.conf