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-30 17:45:30 +11:00
from textual import work
2025-10-27 21:27:50 +11:00
import textual . widgets
2025-10-30 17:45:30 +11:00
from typing import Any
2025-10-27 21:27:50 +11:00
from traceback import format_exception
2025-10-27 18:43:34 +11:00
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 20:18:24 +11:00
# region Lua functions
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
2025-10-27 20:18:24 +11:00
#self.app.bind(key, action_name, description=description, show=show)
self . app . _bindings . bind ( key , action_name , description , show , priority = True )
2025-10-27 18:43:34 +11:00
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 21:27:50 +11:00
def create_widget ( self , widget_type : str , * args ) :
return getattr ( textual . widgets , widget_type ) ( * args )
2025-10-29 21:06:34 +11:00
def run_on_message ( self , event , function ) :
raise NotImplementedError ( " onMessage is not implemented yet. " )
2025-10-27 18:43:34 +11:00
2025-10-27 20:18:24 +11:00
def create_window ( self , title : str , icon : str = " " , show_title : bool = True , can_close : bool = True , can_maximize : bool = True , can_resize : bool = True , start_horizontal : str = " center " , start_vertical : str = " middle " ) :
new_window = Window (
id = title ,
mode = " temporary " if can_close else ' permanent ' ,
allow_maximize = can_maximize ,
allow_resize = can_resize ,
start_open = True ,
icon = icon ,
show_title = show_title ,
starting_horizontal = start_horizontal ,
starting_vertical = start_vertical
)
2025-10-27 21:27:50 +11:00
#self.app.mount(new_window)
2025-10-27 20:18:24 +11:00
return new_window
2025-10-30 17:45:30 +11:00
def create_setting ( self , section_name : str , option_name : str , description : str , option_type : str , default_value : Any , on_changed_func = None ) :
self . app . config_handler . define_plugin_setting ( section_name , option_name , description , option_type , default_value , on_changed_func )
2025-10-27 20:18:24 +11:00
# endregion
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 ,
" runAction " : self . fake_run_action ,
" addBind " : self . add_bind ,
2025-10-27 20:18:24 +11:00
" createAction " : self . create_action ,
2025-10-27 21:27:50 +11:00
" createWindow " : self . create_window ,
" createWidget " : self . create_widget ,
" app " : self . app ,
2025-10-29 21:06:34 +11:00
" onMessage " : self . run_on_message
2025-10-27 21:27:50 +11:00
} ,
2025-10-30 17:45:30 +11:00
" config " : {
" get " : self . app . config_handler . get ,
" defineSetting " : self . create_setting
}
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 :
2025-10-27 21:27:50 +11:00
log . write ( f " [b red]Error in { lua_file_path } : \n " + ' \n ' . join ( format_exception ( 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
2025-10-27 21:27:50 +11:00
if executed_code . run :
try :
executed_code . run ( )
except Exception as e :
log . write ( f " [b red]Runtime error in { lua_file_path } : \n " + " \n " . join ( format_exception ( e ) ) + " [/] " )
self . notify ( " A plugin has created an error. Check the log for more details. " , title = " Lua Error " , severity = " error " , timeout = 10 )
no_errors = False
continue
2025-10-27 13:09:10 +11:00
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-30 12:38:14 +11:00
if no_errors and int ( self . app . config_handler . get ( " plugins " , " log_timeout " ) ) != - 1 :
2025-10-30 17:45:30 +11:00
log . write ( f ' [d]Window will automatically close in { self . app . config_handler . get ( " plugins " , " log_timeout " ) } seconds.[/] ' )
2025-10-30 12:38:14 +11:00
await asyncio . sleep ( int ( self . app . config_handler . get ( " plugins " , " log_timeout " ) ) )
2025-10-27 15:53:54 +11:00
self . close_window ( )
2025-10-27 07:10:23 +11:00
async def on_mount ( self ) :
2025-10-30 12:38:14 +11:00
if bool ( int ( self . app . config_handler . get ( " plugins " , " log " ) ) ) == False :
self . display = " none "
2025-10-27 07:10:23 +11:00
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 )