2025-10-27 07:08:10 +11:00
from textual_window import Window
2025-10-27 18:43:34 +11:00
from textual . widgets import RichLog , Button
2025-10-27 07:10:23 +11:00
from textual import work
2025-10-27 18:43:34 +11:00
from textual . binding import Binding
from lupa import lua51
2025-10-27 07:08:10 +11:00
2025-10-27 07:13:59 +11:00
import os , json , asyncio
2025-10-27 07:08:10 +11:00
class PluginLoader ( Window ) :
def __init__ ( self ) :
super ( ) . __init__ (
id = " Plugin Loader " ,
mode = " permanent " ,
icon = " ⚙️ " ,
2025-10-27 07:13:59 +11:00
starting_horizontal = " right " ,
starting_vertical = " bottom " ,
2025-10-27 15:53:54 +11:00
start_open = True ,
allow_maximize = True
2025-10-27 07:08:10 +11:00
)
2025-10-27 18:43:34 +11:00
def fake_notify ( self , message : str , title : str = None , severity : str = " information " ) :
self . app . notify ( message = message , title = title , severity = severity )
def create_sidebar_button ( self , icon : str ) :
new_button = Button ( icon )
self . app . query_one ( " #sidebar-buttons " ) . mount ( new_button )
def set_theme ( self , theme_name : str ) :
self . app . theme = theme_name
def add_bind ( self , action_name : str , key : str , description : str , show : bool = True ) :
# a bit of a sneaky way of doing things
self . app . bind ( key , action_name , description = description , show = show )
self . app . refresh_bindings ( )
def fake_run_action ( self , action_name : str ) :
getattr ( self . app , f " action_ { action_name } " ) ( )
def create_action ( self , action_name : str , function ) :
def wrapper ( ) :
function ( )
setattr ( self . app , f " action_ { action_name } " , wrapper )
2025-10-27 07:10:23 +11:00
@work
async def find_plugins ( self ) :
2025-10-27 07:08:10 +11:00
log = self . query_one ( RichLog )
2025-10-27 13:11:33 +11:00
2025-10-27 15:53:54 +11:00
no_errors = True
2025-10-27 13:11:33 +11:00
log . write ( " [b]Setting up LUA runtime..[/] " )
self . lua_runtime = lua51 . LuaRuntime ( )
2025-10-27 18:43:34 +11:00
2025-10-27 13:11:33 +11:00
lua_runtime_stuff = {
" ui " : {
2025-10-27 18:43:34 +11:00
" notify " : self . fake_notify ,
2025-10-27 19:29:30 +11:00
#"createSidebarButton": self.create_sidebar_button,
2025-10-27 18:43:34 +11:00
" setTheme " : self . set_theme ,
" runAction " : self . fake_run_action ,
" addBind " : self . add_bind ,
" createAction " : self . create_action
2025-10-27 13:11:33 +11:00
}
}
2025-10-27 07:08:10 +11:00
log . write ( " [b]Finding plugins...[/] " )
# Find all plugins (they're just in the plugins folder for now)
folders = [
os . path . join ( os . path . dirname ( __file__ ) , " plugins " )
]
# path to the folder of all correctly formatted plugins
plugin_paths = [ ]
for folder in folders :
log . write ( f " Searching { folder } ... " )
plugin_folders = os . listdir ( folder )
for plugin_folder in plugin_folders :
plugin_folder = os . path . join ( folder , plugin_folder )
if not os . path . isdir ( plugin_folder ) :
log . write ( f " [d]Ignoring { plugin_folder } because it is not a folder.[/] " )
2025-10-27 15:53:54 +11:00
no_errors = False
2025-10-27 07:08:10 +11:00
continue
if not os . path . isdir ( os . path . join ( plugin_folder , " lua " ) ) :
log . write ( f " [d]Ignoring { plugin_folder } because it has no \" lua \" folder.[/] " )
2025-10-27 15:53:54 +11:00
no_errors = False
2025-10-27 07:08:10 +11:00
continue
if not os . path . isfile ( os . path . join ( plugin_folder , " plugin.json " ) ) :
log . write ( f " [d]Ignoring { plugin_folder } because it has no plugin.json file.[/] " )
2025-10-27 15:53:54 +11:00
no_errors = False
2025-10-27 07:08:10 +11:00
continue
with open ( os . path . join ( plugin_folder , " plugin.json " ) , " r " ) as f :
try :
plugin_json = json . loads ( f . read ( ) )
plugin_json [ " name " ]
plugin_json [ " version " ]
plugin_json [ " author " ]
plugin_json [ " dependencies " ]
except UnicodeDecodeError :
log . write ( f " [d]Ignoring { plugin_folder } because its plugin.json file is unreadable.[/] " )
2025-10-27 15:53:54 +11:00
no_errors = False
2025-10-27 07:08:10 +11:00
continue
except json . JSONDecodeError :
log . write ( f " [d]Ignoring { plugin_folder } because its plugin.json file is malformed.[/] " )
2025-10-27 15:53:54 +11:00
no_errors = False
2025-10-27 07:08:10 +11:00
continue
except KeyError as e :
log . write ( f " [d]Ignoring { plugin_folder } because its plugin.json file is missing the field { e } . " )
2025-10-27 15:53:54 +11:00
no_errors = False
2025-10-27 07:08:10 +11:00
continue
except Exception as e :
log . write ( f " [d]Ignoring { plugin_folder } because of error: { e } .[/] " )
2025-10-27 15:53:54 +11:00
no_errors = False
2025-10-27 07:08:10 +11:00
continue
log . write ( f " [b green]FOUND[/] { plugin_json [ ' name ' ] } ( { plugin_json [ ' version ' ] } ) " )
2025-10-27 13:09:10 +11:00
for lua_file in os . listdir ( os . path . join ( plugin_folder , " lua " ) ) :
lua_file_path = os . path . join ( plugin_folder , f " lua/ { lua_file } " )
with open ( lua_file_path , " r " ) as f :
code = f . read ( )
sandbox = self . lua_runtime . eval ( " {} " )
setfenv = self . lua_runtime . eval ( " setfenv " )
sandbox . print = self . log #self.lua_runtime.globals().print
sandbox . math = self . lua_runtime . globals ( ) . math
sandbox . string = self . lua_runtime . globals ( ) . string
2025-10-27 15:57:34 +11:00
sandbox . tostring = self . lua_runtime . globals ( ) . tostring
sandbox . tonumber = self . lua_runtime . globals ( ) . tonumber
2025-10-27 13:11:33 +11:00
sandbox . berry = lua_runtime_stuff
2025-10-27 13:09:10 +11:00
setfenv ( 0 , sandbox )
2025-10-27 15:53:54 +11:00
try :
executed_code = self . lua_runtime . execute ( code )
except lua51 . LuaError as e :
log . write ( f " [b red]Error in { lua_file_path } : { e } [/] " )
2025-10-27 18:43:34 +11:00
self . notify ( " There was Lua error while loading one your installed plugins. Check the Plugin Loader window for more details. " , title = " Lua Error " , severity = " error " , timeout = 10 )
2025-10-27 15:53:54 +11:00
no_errors = False
continue
2025-10-27 13:09:10 +11:00
if executed_code . init :
executed_code . init ( )
if executed_code . main :
self . run_worker ( thread = True , work = executed_code . main )
2025-10-27 07:08:10 +11:00
plugin_paths . append ( plugin_folder )
2025-10-27 18:43:34 +11:00
log . write ( " \n [b]Done loading plugins![/] " )
2025-10-27 15:53:54 +11:00
if no_errors :
2025-10-27 18:43:34 +11:00
log . write ( " [d]Window will automatically close in 5 seconds.[/] " )
2025-10-27 15:53:54 +11:00
await asyncio . sleep ( 5.0 )
self . close_window ( )
2025-10-27 07:10:23 +11:00
async def on_mount ( self ) :
self . find_plugins ( )
2025-10-27 07:08:10 +11:00
def compose ( self ) :
2025-10-27 15:53:54 +11:00
yield RichLog ( markup = True , id = " plugins-log " , wrap = True )