Skip to content

Commit

Permalink
Merge pull request #6 from D0rkKnight/progs
Browse files Browse the repository at this point in the history
Launch progs from windows
  • Loading branch information
shouhanzen authored Dec 28, 2023
2 parents c8bab4a + 2e9ae56 commit d2bbfb9
Show file tree
Hide file tree
Showing 13 changed files with 383 additions and 82 deletions.
1 change: 1 addition & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ dist
*.spec
*.egg-info
.cache
public/icons/programs/*
1 change: 1 addition & 0 deletions backend/public/attributions.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
default_prog_windows.svg: https://iconoir.com/ (Windows Icon)
1 change: 1 addition & 0 deletions backend/public/icons/default_prog_windows.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ uvicorn
pydantic
spotipy
pyinstaller
pillow
pillow
pywin32
Binary file not shown.
Binary file not shown.
Binary file modified backend/src/__pycache__/main.cpython-311.pyc
Binary file not shown.
182 changes: 182 additions & 0 deletions backend/src/apps_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import subprocess
import platform
import json
import os
import src.extract_icon as extract_icon
import configparser
import shutil
import pythoncom

from PIL import Image
from src.cmd_types import *


def start_app(app: str):
if platform.system() == "Windows":
command = f'start "" "{app}"'
subprocess.Popen(command, shell=True)
else:
raise NotImplementedError


def get_commands():
pythoncom.CoInitialize()

commands = []
commands += get_prog_commands()

pythoncom.CoUninitialize()
return commands


def get_prog_commands():
# opens start menu and gets all programs
if platform.system() == "Windows":
global_path = os.path.join(
os.environ["PROGRAMDATA"],
"Microsoft",
"Windows",
"Start Menu",
)
local_path = os.path.join(
os.environ["APPDATA"],
"Microsoft",
"Windows",
"Start Menu",
)

print("System Start Menu Contents:")
shortcuts = list_shortcuts_windows(global_path) + list_shortcuts_windows(
local_path
)

# print(shortcuts)

# Clear out shortcuts with duplicate names
names = []
unique_shortcuts = []
for shortcut in shortcuts:
trimmed = shortcut["name"].strip()

if trimmed in names:
print("WARNING: Duplicate shortcut name found: " + shortcut["name"])
else:
names.append(trimmed)
unique_shortcuts.append(shortcut)

cmds = []

for shortcut in unique_shortcuts:
# simple_print(shortcut)

# Get icon
icon_path = ""
if shortcut["icon"] != "":
icon_path = shortcut["icon"]
elif shortcut["path"].endswith(".exe"):
icon_path = shortcut["path"]

dest_path, success = load_icon_from_resource_windows(
icon_path, shortcut["name"]
)
# simple_print(icon_path)

remote_icon_path = os.path.join("/", "icons", "default_prog_windows.svg")
if success:
remote_icon_path = dest_path.replace(
os.path.join(os.getcwd(), "public"), ""
)

cmds.append(
Command(
title=f"Run: {shortcut['name']}",
command=lambda path=shortcut["path"]: start_app(path),
description="",
icon=remote_icon_path,
)
)

return cmds
else:
raise NotImplementedError


def load_icon_from_resource_windows(path, fname):
success = False
dest_path = ""
if path.endswith(".exe"):
try:
icon = extract_icon.extract_icon(path, extract_icon.IconSize.LARGE)
print(icon)

# Store icon in public/icons/programs
dest_path = os.path.join(
os.getcwd(), "public", "icons", "programs", fname + ".ico"
)
icon.save(dest_path)

print("Saving icon for " + path + " to " + dest_path + ".")
success = True

except Exception as e:
print(f"Error extracting icon for {path}: {e}")

elif path.endswith(".ico"):
dest_path = os.path.join(
os.getcwd(),
"public",
"icons",
"programs",
fname + ".ico",
)

try:
shutil.copy(
path,
dest_path,
)
success = True
except Exception as e:
print(f"Error copying icon for {path}: {e}")

return dest_path, success


def list_shortcuts_windows(directory):
import win32com.client

out = []

"""
Recursively lists all shortcuts in the given directory and writes them to a log file.
"""
if os.path.exists(directory):
for item in os.listdir(directory):
full_path = os.path.join(directory, item)
if os.path.isdir(full_path):
# Recursively search in directories
out = out + list_shortcuts_windows(full_path)
elif item.lower().endswith(".lnk") or item.lower().endswith(".url"):
icon = "" # If empty, means we extract from executable

if item.lower().endswith(".lnk"):
pywin_client = win32com.client.Dispatch("WScript.Shell")
shortcut = pywin_client.CreateShortCut(full_path)
real_path = shortcut.Targetpath

elif item.lower().endswith(".url"):
config = configparser.ConfigParser()
config.read(full_path)

data = dict(config.items("InternetShortcut"))
print(data)
real_path = data["url"]

if "iconfile" in data:
icon = data["iconfile"]

name = item[:-4].encode("utf-8", "replace").decode("utf-8")

out += [{"name": name, "path": real_path, "icon": icon}]

return out
155 changes: 155 additions & 0 deletions backend/src/extract_icon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Attribs: https://pythonassets.com/posts/extract-icon-from-executable-file-windows/

from ctypes import Array, byref, c_char, memset, sizeof
from ctypes import c_int, c_void_p, POINTER
from ctypes.wintypes import *
from enum import Enum
import ctypes

from PIL import Image


BI_RGB = 0
DIB_RGB_COLORS = 0


class ICONINFO(ctypes.Structure):
_fields_ = [
("fIcon", BOOL),
("xHotspot", DWORD),
("yHotspot", DWORD),
("hbmMask", HBITMAP),
("hbmColor", HBITMAP),
]


class RGBQUAD(ctypes.Structure):
_fields_ = [
("rgbBlue", BYTE),
("rgbGreen", BYTE),
("rgbRed", BYTE),
("rgbReserved", BYTE),
]


class BITMAPINFOHEADER(ctypes.Structure):
_fields_ = [
("biSize", DWORD),
("biWidth", LONG),
("biHeight", LONG),
("biPlanes", WORD),
("biBitCount", WORD),
("biCompression", DWORD),
("biSizeImage", DWORD),
("biXPelsPerMeter", LONG),
("biYPelsPerMeter", LONG),
("biClrUsed", DWORD),
("biClrImportant", DWORD),
]


class BITMAPINFO(ctypes.Structure):
_fields_ = [
("bmiHeader", BITMAPINFOHEADER),
("bmiColors", RGBQUAD * 1),
]


shell32 = ctypes.WinDLL("shell32", use_last_error=True)
user32 = ctypes.WinDLL("user32", use_last_error=True)
gdi32 = ctypes.WinDLL("gdi32", use_last_error=True)

gdi32.CreateCompatibleDC.argtypes = [HDC]
gdi32.CreateCompatibleDC.restype = HDC
gdi32.GetDIBits.argtypes = [HDC, HBITMAP, UINT, UINT, LPVOID, c_void_p, UINT]
gdi32.GetDIBits.restype = c_int
gdi32.DeleteObject.argtypes = [HGDIOBJ]
gdi32.DeleteObject.restype = BOOL
shell32.ExtractIconExW.argtypes = [LPCWSTR, c_int, POINTER(HICON), POINTER(HICON), UINT]
shell32.ExtractIconExW.restype = UINT
user32.GetIconInfo.argtypes = [HICON, POINTER(ICONINFO)]
user32.GetIconInfo.restype = BOOL
user32.DestroyIcon.argtypes = [HICON]
user32.DestroyIcon.restype = BOOL


class IconSize(Enum):
SMALL = 1
LARGE = 2

@staticmethod
def to_wh(size: "IconSize") -> tuple[int, int]:
"""
Return the actual (width, height) values for the specified icon size.
"""
size_table = {IconSize.SMALL: (16, 16), IconSize.LARGE: (32, 32)}
return size_table[size]


def extract_icon(filename: str, size: IconSize) -> Image:
"""
Extract the icon from the specified `filename`, which might be
either an executable or an `.ico` file.
"""
dc: HDC = gdi32.CreateCompatibleDC(0)
if dc == 0:
raise ctypes.WinError()

hicon: HICON = HICON()
extracted_icons: UINT = shell32.ExtractIconExW(
filename,
0,
byref(hicon) if size == IconSize.LARGE else None,
byref(hicon) if size == IconSize.SMALL else None,
1,
)
print(extracted_icons)
if extracted_icons != 1:
raise ctypes.WinError()

def cleanup() -> None:
if icon_info.hbmColor != 0:
gdi32.DeleteObject(icon_info.hbmColor)
if icon_info.hbmMask != 0:
gdi32.DeleteObject(icon_info.hbmMask)
user32.DestroyIcon(hicon)

icon_info: ICONINFO = ICONINFO(0, 0, 0, 0, 0)
if not user32.GetIconInfo(hicon, byref(icon_info)):
cleanup()
raise ctypes.WinError()

w, h = IconSize.to_wh(size)
bmi: BITMAPINFO = BITMAPINFO()
memset(byref(bmi), 0, sizeof(bmi))
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER)
bmi.bmiHeader.biWidth = w
bmi.bmiHeader.biHeight = -h
bmi.bmiHeader.biPlanes = 1
bmi.bmiHeader.biBitCount = 32
bmi.bmiHeader.biCompression = BI_RGB
bmi.bmiHeader.biSizeImage = w * h * 4
bits = ctypes.create_string_buffer(bmi.bmiHeader.biSizeImage)
copied_lines = gdi32.GetDIBits(
dc, icon_info.hbmColor, 0, h, bits, byref(bmi), DIB_RGB_COLORS
)
if copied_lines == 0:
cleanup()
raise ctypes.WinError()

# My code
# Map from BGRA -> RGBA
# Function to swap red and blue bytes in BGRA to get RGBA
def swap_red_blue(bgra_buffer, size):
for i in range(0, size, 4):
# Swap the red and blue bytes (first and third bytes)
bgra_buffer[i], bgra_buffer[i + 2] = bgra_buffer[i + 2], bgra_buffer[i]

# Call the function to modify the bits buffer
swap_red_blue(bits, bmi.bmiHeader.biSizeImage)

mode = "RGBA"
image = Image.frombuffer(mode, (w, h), bits, "raw", mode, 0, 1)

cleanup()
return image
7 changes: 5 additions & 2 deletions backend/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import src.web_interface as web_interface
import src.cmd_interface as cmd_interface
import src.apps_interface as apps_interface

import uvicorn
import subprocess
Expand All @@ -20,7 +21,7 @@

base_commands = CommandList(commands=[])

contributors = [web_interface, spotify_core, cmd_interface]
contributors = [web_interface, spotify_core, cmd_interface, apps_interface]


def build_commands_list():
Expand All @@ -35,7 +36,9 @@ def build_commands_list():
# Assert that command titles are unique
titles = []
for command in out.commands:
assert command.title not in titles
if command.title in titles:
raise ValueError(f"Duplicate command title: {command.title}")

titles.append(command.title)

return out
Expand Down
Loading

0 comments on commit d2bbfb9

Please sign in to comment.