Skip to content

Commit 4e72f7f

Browse files
many major changes
1 parent 8f1aea6 commit 4e72f7f

File tree

13 files changed

+389
-69
lines changed

13 files changed

+389
-69
lines changed

makefile

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# Cross-platform Makefile for building Python project with Nuitka and UV
2+
3+
# Configuration
4+
APP_NAME = fancyfetch
5+
MAIN_SCRIPT = main.py
6+
OUTPUT_DIR = dist
7+
BUILD_DIR = build
8+
9+
# Detect OS
10+
ifeq ($(OS),Windows_NT)
11+
DETECTED_OS := Windows
12+
RM = del /f /q
13+
RMDIR = rmdir /s /q
14+
MKDIR = mkdir
15+
PATHSEP = \\
16+
NULLDEV = nul
17+
EXE_EXT = .exe
18+
else
19+
DETECTED_OS := $(shell uname -s)
20+
RM = rm -f
21+
RMDIR = rm -rf
22+
MKDIR = mkdir -p
23+
PATHSEP = /
24+
NULLDEV = /dev/null
25+
EXE_EXT =
26+
endif
27+
28+
# Modules to include in compilation
29+
INCLUDE_MODULES = src.setup src.constants src.formatting src.shared src.configurationhandler
30+
EXCLUDE_MODULES = src.defaults
31+
32+
# Convert to Nuitka arguments
33+
INCLUDE_ARGS = $(foreach module,$(INCLUDE_MODULES),--include-module=$(module))
34+
EXCLUDE_ARGS = $(foreach module,$(EXCLUDE_MODULES),--nofollow-import-to=$(module))
35+
36+
# Default target
37+
.PHONY: all
38+
all: build
39+
40+
# Install dependencies
41+
.PHONY: install
42+
install:
43+
uv sync
44+
uv add nuitka
45+
46+
# Clean build artifacts (cross-platform)
47+
.PHONY: clean
48+
clean:
49+
ifeq ($(DETECTED_OS),Windows)
50+
-$(RMDIR) "$(BUILD_DIR)" 2>$(NULLDEV)
51+
-$(RMDIR) "$(OUTPUT_DIR)" 2>$(NULLDEV)
52+
-$(RMDIR) *.dist 2>$(NULLDEV)
53+
-$(RMDIR) *.build 2>$(NULLDEV)
54+
-$(RM) "$(APP_NAME)$(EXE_EXT)" 2>$(NULLDEV)
55+
-for /r . %%i in (*.pyc) do $(RM) "%%i" 2>$(NULLDEV)
56+
-for /d /r . %%i in (__pycache__) do $(RMDIR) "%%i" 2>$(NULLDEV)
57+
else
58+
-$(RMDIR) $(BUILD_DIR) $(OUTPUT_DIR) *.dist *.build 2>$(NULLDEV)
59+
-$(RM) $(APP_NAME)$(EXE_EXT) 2>$(NULLDEV)
60+
-find . -name "*.pyc" -delete 2>$(NULLDEV)
61+
-find . -name "__pycache__" -type d -exec rm -rf {} + 2>$(NULLDEV)
62+
endif
63+
64+
# Build the application
65+
.PHONY: build
66+
build: clean install-nuitka
67+
@echo "Building $(APP_NAME) with Nuitka on $(DETECTED_OS)..."
68+
ifeq ($(DETECTED_OS),Windows)
69+
-$(MKDIR) "$(OUTPUT_DIR)" 2>$(NULLDEV)
70+
else
71+
-$(MKDIR) $(OUTPUT_DIR)
72+
endif
73+
uv run nuitka --onefile --standalone --output-filename=$(APP_NAME) --output-dir=$(OUTPUT_DIR) $(INCLUDE_ARGS) $(EXCLUDE_ARGS) --show-progress --assume-yes-for-downloads $(MAIN_SCRIPT)
74+
75+
.PHONY: just
76+
just:
77+
nuitka --standalone --output-filename=fancyfetch --output-dir=dist --include-module=setup --include-module=constants --include-module=formatting --include-module=shared --include-module=configurationhandler --nofollow-import-to=src.defaults --show-progress --assume-yes-for-downloads main.py
78+
79+
# Ensure Nuitka is installed
80+
.PHONY: install-nuitka
81+
install-nuitka:
82+
@echo "Checking for Nuitka..."
83+
@uv run python -c "import nuitka" 2>$(NULLDEV) || uv add nuitka
84+
85+
# Build with debug information
86+
.PHONY: build-debug
87+
build-debug: clean install-nuitka
88+
@echo "Building $(APP_NAME) with debug info..."
89+
ifeq ($(DETECTED_OS),Windows)
90+
-$(MKDIR) "$(OUTPUT_DIR)" 2>$(NULLDEV)
91+
else
92+
-$(MKDIR) $(OUTPUT_DIR)
93+
endif
94+
uv run nuitka \
95+
--onefile \
96+
--standalone \
97+
--output-filename=$(APP_NAME) \
98+
--output-dir=$(OUTPUT_DIR) \
99+
$(INCLUDE_ARGS) \
100+
$(EXCLUDE_ARGS) \
101+
--show-progress \
102+
--assume-yes-for-downloads \
103+
--debug \
104+
--show-modules \
105+
--report=compilation-report.xml \
106+
$(MAIN_SCRIPT)
107+
108+
# Fast build (no optimization)
109+
.PHONY: build-fast
110+
build-fast: clean install-nuitka
111+
@echo "Building $(APP_NAME) (fast mode)..."
112+
ifeq ($(DETECTED_OS),Windows)
113+
-$(MKDIR) "$(OUTPUT_DIR)" 2>$(NULLDEV)
114+
else
115+
-$(MKDIR) $(OUTPUT_DIR)
116+
endif
117+
uv run nuitka \
118+
--onefile \
119+
--standalone \
120+
--output-filename=$(APP_NAME) \
121+
--output-dir=$(OUTPUT_DIR) \
122+
$(INCLUDE_ARGS) \
123+
$(EXCLUDE_ARGS) \
124+
--show-progress \
125+
--assume-yes-for-downloads \
126+
--no-prefer-source-code \
127+
$(MAIN_SCRIPT)
128+
129+
# Build optimized version
130+
.PHONY: build-optimized
131+
build-optimized: clean install-nuitka
132+
@echo "Building optimized $(APP_NAME)..."
133+
ifeq ($(DETECTED_OS),Windows)
134+
-$(MKDIR) "$(OUTPUT_DIR)" 2>$(NULLDEV)
135+
else
136+
-$(MKDIR) $(OUTPUT_DIR)
137+
endif
138+
uv run nuitka \
139+
--onefile \
140+
--standalone \
141+
--output-filename=$(APP_NAME) \
142+
--output-dir=$(OUTPUT_DIR) \
143+
$(INCLUDE_ARGS) \
144+
$(EXCLUDE_ARGS) \
145+
--show-progress \
146+
--assume-yes-for-downloads \
147+
--lto=yes \
148+
--enable-plugin=anti-bloat \
149+
$(MAIN_SCRIPT)
150+
151+
# Test the built application
152+
.PHONY: test
153+
test: build
154+
@echo "Testing built application..."
155+
ifeq ($(DETECTED_OS),Windows)
156+
cd $(OUTPUT_DIR) && $(APP_NAME).exe --version || $(APP_NAME).exe --help || echo Test completed
157+
else
158+
cd $(OUTPUT_DIR) && ./$(APP_NAME) --version || ./$(APP_NAME) --help || echo "Test completed"
159+
endif
160+
161+
# Show build information
162+
.PHONY: info
163+
info:
164+
@echo "Build Configuration:"
165+
@echo " Detected OS: $(DETECTED_OS)"
166+
@echo " App Name: $(APP_NAME)"
167+
@echo " Main Script: $(MAIN_SCRIPT)"
168+
@echo " Output Directory: $(OUTPUT_DIR)"
169+
@echo " Include Modules: $(INCLUDE_MODULES)"
170+
@echo ""
171+
@echo "Available targets:"
172+
@echo " build - Standard build"
173+
@echo " build-debug - Build with debug info"
174+
@echo " build-fast - Fast build (less optimization)"
175+
@echo " build-optimized- Highly optimized build"
176+
@echo " test - Test the built application"
177+
@echo " clean - Clean build artifacts"
178+
@echo " install - Install dependencies"
179+
180+
# Development build
181+
.PHONY: dev
182+
dev: build-fast test

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,11 @@ readme = "README.md"
66
requires-python = ">=3.12"
77
dependencies = [
88
"json5>=0.12.0",
9+
"nuitka>=2.7.11",
910
"rich>=14.0.0",
1011
]
12+
13+
[dependency-groups]
14+
dev = [
15+
"nuitka>=2.7.11",
16+
]

src/configurationhandler.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import constants as FancyfetchConstants
2-
import json5 as JSONFiveLibrary
1+
import shared as FancyfetchShared
2+
import json5 as JSONFiveLibrary # type: ignore # This line commonly errors stating that json5 cannot be found, oddly only when working with Nuitka, to my experience.
3+
# Though json5 is perfectly available to the code, Nuitka finds and includes it, so I have no clue what is happening here.
34

45
def FetchConfiguration():
5-
with open(FancyfetchConstants.ConfigurationFile, 'r', encoding='UTF-8') as FilePointer:
6+
with open(FancyfetchShared.ConfigurationFile, 'r', encoding='UTF-8') as FilePointer:
67
try:
78
return JSONFiveLibrary.load(FilePointer) # Return the configuration, parsed into a Python dictionary.
89
except Exception as e:

src/constants.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,30 @@
1+
import importlib.util as ImportLibraryUtilities
12
import pathlib as PathLibrary
23

3-
ConfigurationDirectory = PathLibrary.Path("~/.config/fancyfetch").expanduser()
4-
ConfigurationFile = ConfigurationDirectory / "configuration.json5"
5-
WidgetsDirectory = ConfigurationDirectory / "widgets"
6-
7-
DefaultConfiguationDirectory = PathLibrary.Path(__file__).parent / "defaults"
8-
DefaultConfigurationFile = DefaultConfiguationDirectory / "configuration.json5"
9-
DefaultWidgetsDirectory = DefaultConfiguationDirectory / "widgets"
4+
def LoadWidget(Directory, Filename):
5+
if not Filename.endswith('.py'):
6+
Filename += '.py'
7+
8+
FilePath = PathLibrary.Path(Directory) / Filename
9+
10+
if not FilePath.exists():
11+
print(f"File {Filename} not found in {Directory}")
12+
raise ValueError(f"You attempted to use the constant {Filename.rstrip('.py')}, but a constant definition for it could not be found, it must be a direct child of {Directory}!")
13+
14+
Spec = ImportLibraryUtilities.spec_from_file_location("temp_module", FilePath)
15+
if Spec is None:
16+
raise ValueError(f"An error occured while processing the definition for the constant {Filename.rstrip('.py')}. A spec for the file could not be generated.")
17+
18+
Module = ImportLibraryUtilities.module_from_spec(Spec)
19+
Spec.loader.exec_module(Module) # type: ignore
20+
21+
if not hasattr(Module, 'Widget'):
22+
raise ValueError(f"An error occured while processing the definition for the constant {Filename.rstrip('.py')}. Widget definitions must define the class `Widget`.")
23+
24+
WidgetClass = getattr(Module, 'Widget')
25+
WidgetInstance = WidgetClass()
26+
27+
if not hasattr(WidgetInstance, 'Get'):
28+
raise ValueError(f"An error occured while processing the definition for the constant {Filename.rstrip('.py')}. Widget classes must define the method `Get`.")
29+
30+
return WidgetInstance.Get()

src/defaults/configuration.json5

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@
44

55

66

7-
// As part of Fancyfetch's limitless customisability, you can create your own widgets.
8-
// Beware: Widgets execute code, make sure you know what the code will do.
7+
// As part of Fancyfetch's limitless customisability, you can create your own constants.
8+
// A constant is a piece of Python code that is executed by fancyfetch, the value it returns is what is used in your layout!
9+
// Beware: Constants execute code, make sure you know what the code will do.
910

10-
// Fancyfetch is not responsible for any damage you, or someone else cause using custom widgets!
11+
// Fancyfetch is not responsible for any damage you, or someone else cause using custom constants!
1112

12-
// To make a widget, make a Python file in the `widgets` directory.
13-
// The file's name (excluding `.py`) is what the widget will be called.
13+
// To make a constant, make a Python file in the `constants` directory.
14+
// The file's name (excluding `.py`) is what the constant will be called.
1415
// The file must contain a class named `Widget`, which must have a method named `Get()`.
15-
// `Get()` will be provided no arguments, but should return something that can be converted to a string, this is the value of the widget.
16+
// `Get()` will be provided no arguments, but should return something that can be converted to a string, this is the value of the constant.
1617

17-
// The order in which to display widgets.
18+
// The order in which to display constants.
1819
"layout":[
1920
"os",
2021
"hostname",
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ class Widget: # Must be named Widget.
22
def __init__(self):
33
# It is advised that you process your most intensive logic here.
44
import platform
5-
self.Value = "<:gold:>Hostname: <:white:>"+platform.node()
5+
self.Value = f"<:gold:>Hostname: <:white:> {platform.node()}"
66

77
def Get(self):return self.Value # Must be named Get, but may return anything that can be converted to a string.

src/dottest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from rich import print
2+
3+
print("Catppuccin Mocha colour swatch styles:")
4+
print("CIRCLE : [#f38ba8]⬤[/#f38ba8] [#fab387]⬤[/#fab387] [#f9e2af]⬤[/#f9e2af] [#a6e3a1]⬤[/#a6e3a1] [#89b4fa]⬤[/#89b4fa] [#b4befe]⬤[/#b4befe]")
5+
print("DIAMOND : [#f38ba8]◆[/#f38ba8] [#fab387]◆[/#fab387] [#f9e2af]◆[/#f9e2af] [#a6e3a1]◆[/#a6e3a1] [#89b4fa]◆[/#89b4fa] [#b4befe]◆[/#b4befe]")
6+
print("SQUARE : [#f38ba8]■[/#f38ba8] [#fab387]■[/#fab387] [#f9e2af]■[/#f9e2af] [#a6e3a1]■[/#a6e3a1] [#89b4fa]■[/#89b4fa] [#b4befe]■[/#b4befe]")

src/main.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import setup as FancyfetchSetup
2-
import constants as FancyfetchConstants
2+
import shared as FancyfetchShared
33
import configurationhandler as FancyfetchConfigurationHandler
44
import formatting as FancyfetchFormatting
5-
import widgets as FancyfetchWidgets
5+
import constants as FancyfetchConstants
66

77
import shutil as ShellUtilityLibrary
88
import os as OperatingSystemLibrary
99
import argparse as ArgumentParserLibrary
1010

11+
from sys import exit # Replaces `exit` as provided by the internal Python `site` library, preventing errors in compiled executables.
12+
1113
def GetChildren(Directory):
1214
try:
1315
return OperatingSystemLibrary.listdir(Directory)
@@ -18,13 +20,13 @@ def CompareChildren(OriginalDirectory, Directory):
1820
Files = set(GetChildren(Directory))
1921
return OriginalFiles - Files
2022
def HasConfigurationBeenChanged():
21-
if (not FancyfetchConstants.WidgetsDirectory.exists()) or (not FancyfetchConstants.ConfigurationFile.exists()):
23+
if (not FancyfetchShared.ConstantsDirectory.exists()) or (not FancyfetchShared.ConfigurationFile.exists()):
2224
FancyfetchSetup.EnsureConfiguration()
2325
print("Your configuration has been regenerated!")
2426
exit(0)
25-
if len(CompareChildren(FancyfetchConstants.WidgetsDirectory, FancyfetchConstants.DefaultWidgetsDirectory)) > 0:
27+
if len(CompareChildren(FancyfetchShared.ConstantsDirectory, FancyfetchShared.DefaultConstantsDirectory)) > 0:
2628
return True
27-
if open(FancyfetchConstants.ConfigurationFile, "r").read() != open(FancyfetchConstants.DefaultConfigurationFile, "r").read():
29+
if open(FancyfetchShared.ConfigurationFile, "r").read() != open(FancyfetchShared.DefaultConfigurationFile, "r").read():
2830
return True
2931

3032
def OPTION_RegenerateConfiguration():
@@ -36,7 +38,7 @@ def OPTION_RegenerateConfiguration():
3638
print("Type 'yes' to confirm, or anything else to cancel.")
3739
Confirmation = input().strip().lower()
3840
if Confirmation.lower().strip() in "yes":
39-
ShellUtilityLibrary.rmtree(FancyfetchConstants.ConfigurationDirectory, ignore_errors=True)
41+
ShellUtilityLibrary.rmtree(FancyfetchShared.ConfigurationDirectory, ignore_errors=True)
4042
FancyfetchSetup.EnsureConfiguration()
4143
print("Your configuration has been regenerated!")
4244
exit(0)
@@ -86,11 +88,19 @@ def Main():
8688
CONFIG_Spacing = Configuration.get("spacing", 5) # Default to 5 if not set.
8789
CONFIG_ASCII = Configuration.get("ascii", ["You have no ASCII defined!","Set the 'ascii' key in your config!","Or run fancyfetch with the '--regen'/'-r' flag!"]) # Default to a warning if not set.
8890

89-
CONFIG_Layout = Configuration.get("layout", ["hello","datetime","credits"]) # Default to ["hello", "datetime"] if not set.
91+
CONFIG_Layout = Configuration.get("layout", ["os","hostname"]) # Default to ["hello", "datetime"] if not set.
9092

9193
ASCII = CONFIG_ASCII if CONFIG_DisplayASCII else []
94+
Layout = []
9295
try:
93-
Layout = [FancyfetchWidgets.LoadWidget(str(FancyfetchConstants.WidgetsDirectory), X) for X in CONFIG_Layout]
96+
for X in CONFIG_Layout:
97+
Current = FancyfetchConstants.LoadWidget(str(FancyfetchShared.ConstantsDirectory),X)
98+
if isinstance(Current, str):
99+
Current = Current.replace("\r\n","\n").split("\n")
100+
Layout = Layout + Current
101+
elif isinstance(Current, list):
102+
for I in Current:
103+
Layout.append(I.replace("\r\n","\n").split("\n"))
94104
except ValueError as e:
95105
print(e.args[0])
96106
exit(1)

src/setup.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import shutil as ShellUtilityLibrary
22
import os as OperatingSystemLibrary
33

4-
import constants as FancyfetchConstants
4+
import shared as FancyfetchShared
55

66
# Functions
77
def EnsureConfigurationDirectory():
8-
FancyfetchConstants.ConfigurationDirectory.mkdir(exist_ok=True, parents=True)
8+
FancyfetchShared.ConfigurationDirectory.mkdir(exist_ok=True, parents=True)
99

1010
def CopyDirectoryContents(SourceDirectory, DestinationDirectory):
1111
for Item in OperatingSystemLibrary.listdir(SourceDirectory):
@@ -19,7 +19,7 @@ def CopyDirectoryContents(SourceDirectory, DestinationDirectory):
1919
ShellUtilityLibrary.copy(SourceItem, DestinationItem)
2020

2121
def EnsureConfiguration():
22-
if not FancyfetchConstants.ConfigurationDirectory.exists():
22+
if not FancyfetchShared.ConfigurationFile.exists():
2323
EnsureConfigurationDirectory()
24-
CopyDirectoryContents(FancyfetchConstants.DefaultConfiguationDirectory, FancyfetchConstants.ConfigurationDirectory)
24+
CopyDirectoryContents(FancyfetchShared.DefaultConfiguationDirectory, FancyfetchShared.ConfigurationDirectory)
2525

0 commit comments

Comments
 (0)