add basic plugin capability

This commit is contained in:
Chrys 2024-10-21 02:18:04 +02:00
parent 8c21412b54
commit f17efc3da8
53 changed files with 3746 additions and 0 deletions

79
m4/build-to-host.m4 Normal file
View File

@ -0,0 +1,79 @@
# build-to-host.m4 serial 3
dnl Copyright (C) 2023-2024 Free Software Foundation, Inc.
dnl This file is free software; the Free Software Foundation
dnl gives unlimited permission to copy and/or distribute it,
dnl with or without modifications, as long as this notice is preserved.
dnl Written by Bruno Haible.
dnl When the build environment ($build_os) is different from the target runtime
dnl environment ($host_os), file names may need to be converted from the build
dnl environment syntax to the target runtime environment syntax. This is
dnl because the Makefiles are executed (mostly) by build environment tools and
dnl therefore expect file names in build environment syntax, whereas the runtime
dnl expects file names in target runtime environment syntax.
dnl
dnl For example, if $build_os = cygwin and $host_os = mingw32, filenames need
dnl be converted from Cygwin syntax to native Windows syntax:
dnl /cygdrive/c/foo/bar -> C:\foo\bar
dnl /usr/local/share -> C:\cygwin64\usr\local\share
dnl
dnl gl_BUILD_TO_HOST([somedir])
dnl This macro takes as input an AC_SUBSTed variable 'somedir', which must
dnl already have its final value assigned, and produces two additional
dnl AC_SUBSTed variables 'somedir_c' and 'somedir_c_make', that designate the
dnl same file name value, just in different syntax:
dnl - somedir_c is the file name in target runtime environment syntax,
dnl as a C string (starting and ending with a double-quote,
dnl and with escaped backslashes and double-quotes in
dnl between).
dnl - somedir_c_make is the same thing, escaped for use in a Makefile.
AC_DEFUN([gl_BUILD_TO_HOST],
[
AC_REQUIRE([AC_CANONICAL_BUILD])
AC_REQUIRE([AC_CANONICAL_HOST])
AC_REQUIRE([gl_BUILD_TO_HOST_INIT])
dnl Define somedir_c.
gl_final_[$1]="$[$1]"
dnl Translate it from build syntax to host syntax.
case "$build_os" in
cygwin*)
case "$host_os" in
mingw* | windows*)
gl_final_[$1]=`cygpath -w "$gl_final_[$1]"` ;;
esac
;;
esac
dnl Convert it to C string syntax.
[$1]_c=`printf '%s\n' "$gl_final_[$1]" | sed -e "$gl_sed_double_backslashes" -e "$gl_sed_escape_doublequotes" | tr -d "$gl_tr_cr"`
[$1]_c='"'"$[$1]_c"'"'
AC_SUBST([$1_c])
dnl Define somedir_c_make.
[$1]_c_make=`printf '%s\n' "$[$1]_c" | sed -e "$gl_sed_escape_for_make_1" -e "$gl_sed_escape_for_make_2" | tr -d "$gl_tr_cr"`
dnl Use the substituted somedir variable, when possible, so that the user
dnl may adjust somedir a posteriori when there are no special characters.
if test "$[$1]_c_make" = '\"'"${gl_final_[$1]}"'\"'; then
[$1]_c_make='\"$([$1])\"'
fi
AC_SUBST([$1_c_make])
])
dnl Some initializations for gl_BUILD_TO_HOST.
AC_DEFUN([gl_BUILD_TO_HOST_INIT],
[
gl_sed_double_backslashes='s/\\/\\\\/g'
gl_sed_escape_doublequotes='s/"/\\"/g'
changequote(,)dnl
gl_sed_escape_for_make_1="s,\\([ \"&'();<>\\\\\`|]\\),\\\\\\1,g"
changequote([,])dnl
gl_sed_escape_for_make_2='s,\$,\\$$,g'
dnl Find out how to remove carriage returns from output. Solaris /usr/ucb/tr
dnl does not understand '\r'.
case `echo r | tr -d '\r'` in
'') gl_tr_cr='\015' ;;
*) gl_tr_cr='\r' ;;
esac
])

527
m4/host-cpu-c-abi.m4 Normal file
View File

@ -0,0 +1,527 @@
# host-cpu-c-abi.m4 serial 17
dnl Copyright (C) 2002-2024 Free Software Foundation, Inc.
dnl This file is free software; the Free Software Foundation
dnl gives unlimited permission to copy and/or distribute it,
dnl with or without modifications, as long as this notice is preserved.
dnl From Bruno Haible and Sam Steingold.
dnl Sets the HOST_CPU variable to the canonical name of the CPU.
dnl Sets the HOST_CPU_C_ABI variable to the canonical name of the CPU with its
dnl C language ABI (application binary interface).
dnl Also defines __${HOST_CPU}__ and __${HOST_CPU_C_ABI}__ as C macros in
dnl config.h.
dnl
dnl This canonical name can be used to select a particular assembly language
dnl source file that will interoperate with C code on the given host.
dnl
dnl For example:
dnl * 'i386' and 'sparc' are different canonical names, because code for i386
dnl will not run on SPARC CPUs and vice versa. They have different
dnl instruction sets.
dnl * 'sparc' and 'sparc64' are different canonical names, because code for
dnl 'sparc' and code for 'sparc64' cannot be linked together: 'sparc' code
dnl contains 32-bit instructions, whereas 'sparc64' code contains 64-bit
dnl instructions. A process on a SPARC CPU can be in 32-bit mode or in 64-bit
dnl mode, but not both.
dnl * 'mips' and 'mipsn32' are different canonical names, because they use
dnl different argument passing and return conventions for C functions, and
dnl although the instruction set of 'mips' is a large subset of the
dnl instruction set of 'mipsn32'.
dnl * 'mipsn32' and 'mips64' are different canonical names, because they use
dnl different sizes for the C types like 'int' and 'void *', and although
dnl the instruction sets of 'mipsn32' and 'mips64' are the same.
dnl * The same canonical name is used for different endiannesses. You can
dnl determine the endianness through preprocessor symbols:
dnl - 'arm': test __ARMEL__.
dnl - 'mips', 'mipsn32', 'mips64': test _MIPSEB vs. _MIPSEL.
dnl - 'powerpc64': test _BIG_ENDIAN vs. _LITTLE_ENDIAN.
dnl * The same name 'i386' is used for CPUs of type i386, i486, i586
dnl (Pentium), AMD K7, Pentium II, Pentium IV, etc., because
dnl - Instructions that do not exist on all of these CPUs (cmpxchg,
dnl MMX, SSE, SSE2, 3DNow! etc.) are not frequently used. If your
dnl assembly language source files use such instructions, you will
dnl need to make the distinction.
dnl - Speed of execution of the common instruction set is reasonable across
dnl the entire family of CPUs. If you have assembly language source files
dnl that are optimized for particular CPU types (like GNU gmp has), you
dnl will need to make the distinction.
dnl See <https://en.wikipedia.org/wiki/X86_instruction_listings>.
AC_DEFUN([gl_HOST_CPU_C_ABI],
[
AC_REQUIRE([AC_CANONICAL_HOST])
AC_REQUIRE([gl_C_ASM])
AC_CACHE_CHECK([host CPU and C ABI], [gl_cv_host_cpu_c_abi],
[case "$host_cpu" in
changequote(,)dnl
i[34567]86 )
changequote([,])dnl
gl_cv_host_cpu_c_abi=i386
;;
x86_64 )
# On x86_64 systems, the C compiler may be generating code in one of
# these ABIs:
# - 64-bit instruction set, 64-bit pointers, 64-bit 'long': x86_64.
# - 64-bit instruction set, 64-bit pointers, 32-bit 'long': x86_64
# with native Windows (mingw, MSVC).
# - 64-bit instruction set, 32-bit pointers, 32-bit 'long': x86_64-x32.
# - 32-bit instruction set, 32-bit pointers, 32-bit 'long': i386.
AC_COMPILE_IFELSE(
[AC_LANG_SOURCE(
[[#if (defined __x86_64__ || defined __amd64__ \
|| defined _M_X64 || defined _M_AMD64)
int ok;
#else
error fail
#endif
]])],
[AC_COMPILE_IFELSE(
[AC_LANG_SOURCE(
[[#if defined __ILP32__ || defined _ILP32
int ok;
#else
error fail
#endif
]])],
[gl_cv_host_cpu_c_abi=x86_64-x32],
[gl_cv_host_cpu_c_abi=x86_64])],
[gl_cv_host_cpu_c_abi=i386])
;;
changequote(,)dnl
alphaev[4-8] | alphaev56 | alphapca5[67] | alphaev6[78] )
changequote([,])dnl
gl_cv_host_cpu_c_abi=alpha
;;
arm* | aarch64 )
# Assume arm with EABI.
# On arm64 systems, the C compiler may be generating code in one of
# these ABIs:
# - aarch64 instruction set, 64-bit pointers, 64-bit 'long': arm64.
# - aarch64 instruction set, 32-bit pointers, 32-bit 'long': arm64-ilp32.
# - 32-bit instruction set, 32-bit pointers, 32-bit 'long': arm or armhf.
AC_COMPILE_IFELSE(
[AC_LANG_SOURCE(
[[#ifdef __aarch64__
int ok;
#else
error fail
#endif
]])],
[AC_COMPILE_IFELSE(
[AC_LANG_SOURCE(
[[#if defined __ILP32__ || defined _ILP32
int ok;
#else
error fail
#endif
]])],
[gl_cv_host_cpu_c_abi=arm64-ilp32],
[gl_cv_host_cpu_c_abi=arm64])],
[# Don't distinguish little-endian and big-endian arm, since they
# don't require different machine code for simple operations and
# since the user can distinguish them through the preprocessor
# defines __ARMEL__ vs. __ARMEB__.
# But distinguish arm which passes floating-point arguments and
# return values in integer registers (r0, r1, ...) - this is
# gcc -mfloat-abi=soft or gcc -mfloat-abi=softfp - from arm which
# passes them in float registers (s0, s1, ...) and double registers
# (d0, d1, ...) - this is gcc -mfloat-abi=hard. GCC 4.6 or newer
# sets the preprocessor defines __ARM_PCS (for the first case) and
# __ARM_PCS_VFP (for the second case), but older GCC does not.
echo 'double ddd; void func (double dd) { ddd = dd; }' > conftest.c
# Look for a reference to the register d0 in the .s file.
AC_TRY_COMMAND(${CC-cc} $CFLAGS $CPPFLAGS $gl_c_asm_opt conftest.c) >/dev/null 2>&1
if LC_ALL=C grep 'd0,' conftest.$gl_asmext >/dev/null; then
gl_cv_host_cpu_c_abi=armhf
else
gl_cv_host_cpu_c_abi=arm
fi
rm -f conftest*
])
;;
hppa1.0 | hppa1.1 | hppa2.0* | hppa64 )
# On hppa, the C compiler may be generating 32-bit code or 64-bit
# code. In the latter case, it defines _LP64 and __LP64__.
AC_COMPILE_IFELSE(
[AC_LANG_SOURCE(
[[#ifdef __LP64__
int ok;
#else
error fail
#endif
]])],
[gl_cv_host_cpu_c_abi=hppa64],
[gl_cv_host_cpu_c_abi=hppa])
;;
ia64* )
# On ia64 on HP-UX, the C compiler may be generating 64-bit code or
# 32-bit code. In the latter case, it defines _ILP32.
AC_COMPILE_IFELSE(
[AC_LANG_SOURCE(
[[#ifdef _ILP32
int ok;
#else
error fail
#endif
]])],
[gl_cv_host_cpu_c_abi=ia64-ilp32],
[gl_cv_host_cpu_c_abi=ia64])
;;
mips* )
# We should also check for (_MIPS_SZPTR == 64), but gcc keeps this
# at 32.
AC_COMPILE_IFELSE(
[AC_LANG_SOURCE(
[[#if defined _MIPS_SZLONG && (_MIPS_SZLONG == 64)
int ok;
#else
error fail
#endif
]])],
[gl_cv_host_cpu_c_abi=mips64],
[# In the n32 ABI, _ABIN32 is defined, _ABIO32 is not defined (but
# may later get defined by <sgidefs.h>), and _MIPS_SIM == _ABIN32.
# In the 32 ABI, _ABIO32 is defined, _ABIN32 is not defined (but
# may later get defined by <sgidefs.h>), and _MIPS_SIM == _ABIO32.
AC_COMPILE_IFELSE(
[AC_LANG_SOURCE(
[[#if (_MIPS_SIM == _ABIN32)
int ok;
#else
error fail
#endif
]])],
[gl_cv_host_cpu_c_abi=mipsn32],
[gl_cv_host_cpu_c_abi=mips])])
;;
powerpc* )
# Different ABIs are in use on AIX vs. Mac OS X vs. Linux,*BSD.
# No need to distinguish them here; the caller may distinguish
# them based on the OS.
# On powerpc64 systems, the C compiler may still be generating
# 32-bit code. And on powerpc-ibm-aix systems, the C compiler may
# be generating 64-bit code.
AC_COMPILE_IFELSE(
[AC_LANG_SOURCE(
[[#if defined __powerpc64__ || defined __LP64__
int ok;
#else
error fail
#endif
]])],
[# On powerpc64, there are two ABIs on Linux: The AIX compatible
# one and the ELFv2 one. The latter defines _CALL_ELF=2.
AC_COMPILE_IFELSE(
[AC_LANG_SOURCE(
[[#if defined _CALL_ELF && _CALL_ELF == 2
int ok;
#else
error fail
#endif
]])],
[gl_cv_host_cpu_c_abi=powerpc64-elfv2],
[gl_cv_host_cpu_c_abi=powerpc64])
],
[gl_cv_host_cpu_c_abi=powerpc])
;;
rs6000 )
gl_cv_host_cpu_c_abi=powerpc
;;
riscv32 | riscv64 )
# There are 2 architectures (with variants): rv32* and rv64*.
AC_COMPILE_IFELSE(
[AC_LANG_SOURCE(
[[#if __riscv_xlen == 64
int ok;
#else
error fail
#endif
]])],
[cpu=riscv64],
[cpu=riscv32])
# There are 6 ABIs: ilp32, ilp32f, ilp32d, lp64, lp64f, lp64d.
# Size of 'long' and 'void *':
AC_COMPILE_IFELSE(
[AC_LANG_SOURCE(
[[#if defined __LP64__
int ok;
#else
error fail
#endif
]])],
[main_abi=lp64],
[main_abi=ilp32])
# Float ABIs:
# __riscv_float_abi_double:
# 'float' and 'double' are passed in floating-point registers.
# __riscv_float_abi_single:
# 'float' are passed in floating-point registers.
# __riscv_float_abi_soft:
# No values are passed in floating-point registers.
AC_COMPILE_IFELSE(
[AC_LANG_SOURCE(
[[#if defined __riscv_float_abi_double
int ok;
#else
error fail
#endif
]])],
[float_abi=d],
[AC_COMPILE_IFELSE(
[AC_LANG_SOURCE(
[[#if defined __riscv_float_abi_single
int ok;
#else
error fail
#endif
]])],
[float_abi=f],
[float_abi=''])
])
gl_cv_host_cpu_c_abi="${cpu}-${main_abi}${float_abi}"
;;
s390* )
# On s390x, the C compiler may be generating 64-bit (= s390x) code
# or 31-bit (= s390) code.
AC_COMPILE_IFELSE(
[AC_LANG_SOURCE(
[[#if defined __LP64__ || defined __s390x__
int ok;
#else
error fail
#endif
]])],
[gl_cv_host_cpu_c_abi=s390x],
[gl_cv_host_cpu_c_abi=s390])
;;
sparc | sparc64 )
# UltraSPARCs running Linux have `uname -m` = "sparc64", but the
# C compiler still generates 32-bit code.
AC_COMPILE_IFELSE(
[AC_LANG_SOURCE(
[[#if defined __sparcv9 || defined __arch64__
int ok;
#else
error fail
#endif
]])],
[gl_cv_host_cpu_c_abi=sparc64],
[gl_cv_host_cpu_c_abi=sparc])
;;
*)
gl_cv_host_cpu_c_abi="$host_cpu"
;;
esac
])
dnl In most cases, $HOST_CPU and $HOST_CPU_C_ABI are the same.
HOST_CPU=`echo "$gl_cv_host_cpu_c_abi" | sed -e 's/-.*//'`
HOST_CPU_C_ABI="$gl_cv_host_cpu_c_abi"
AC_SUBST([HOST_CPU])
AC_SUBST([HOST_CPU_C_ABI])
# This was
# AC_DEFINE_UNQUOTED([__${HOST_CPU}__])
# AC_DEFINE_UNQUOTED([__${HOST_CPU_C_ABI}__])
# earlier, but KAI C++ 3.2d doesn't like this.
sed -e 's/-/_/g' >> confdefs.h <<EOF
#ifndef __${HOST_CPU}__
#define __${HOST_CPU}__ 1
#endif
#ifndef __${HOST_CPU_C_ABI}__
#define __${HOST_CPU_C_ABI}__ 1
#endif
EOF
AH_TOP([/* CPU and C ABI indicator */
#ifndef __i386__
#undef __i386__
#endif
#ifndef __x86_64_x32__
#undef __x86_64_x32__
#endif
#ifndef __x86_64__
#undef __x86_64__
#endif
#ifndef __alpha__
#undef __alpha__
#endif
#ifndef __arm__
#undef __arm__
#endif
#ifndef __armhf__
#undef __armhf__
#endif
#ifndef __arm64_ilp32__
#undef __arm64_ilp32__
#endif
#ifndef __arm64__
#undef __arm64__
#endif
#ifndef __hppa__
#undef __hppa__
#endif
#ifndef __hppa64__
#undef __hppa64__
#endif
#ifndef __ia64_ilp32__
#undef __ia64_ilp32__
#endif
#ifndef __ia64__
#undef __ia64__
#endif
#ifndef __loongarch64__
#undef __loongarch64__
#endif
#ifndef __m68k__
#undef __m68k__
#endif
#ifndef __mips__
#undef __mips__
#endif
#ifndef __mipsn32__
#undef __mipsn32__
#endif
#ifndef __mips64__
#undef __mips64__
#endif
#ifndef __powerpc__
#undef __powerpc__
#endif
#ifndef __powerpc64__
#undef __powerpc64__
#endif
#ifndef __powerpc64_elfv2__
#undef __powerpc64_elfv2__
#endif
#ifndef __riscv32__
#undef __riscv32__
#endif
#ifndef __riscv64__
#undef __riscv64__
#endif
#ifndef __riscv32_ilp32__
#undef __riscv32_ilp32__
#endif
#ifndef __riscv32_ilp32f__
#undef __riscv32_ilp32f__
#endif
#ifndef __riscv32_ilp32d__
#undef __riscv32_ilp32d__
#endif
#ifndef __riscv64_ilp32__
#undef __riscv64_ilp32__
#endif
#ifndef __riscv64_ilp32f__
#undef __riscv64_ilp32f__
#endif
#ifndef __riscv64_ilp32d__
#undef __riscv64_ilp32d__
#endif
#ifndef __riscv64_lp64__
#undef __riscv64_lp64__
#endif
#ifndef __riscv64_lp64f__
#undef __riscv64_lp64f__
#endif
#ifndef __riscv64_lp64d__
#undef __riscv64_lp64d__
#endif
#ifndef __s390__
#undef __s390__
#endif
#ifndef __s390x__
#undef __s390x__
#endif
#ifndef __sh__
#undef __sh__
#endif
#ifndef __sparc__
#undef __sparc__
#endif
#ifndef __sparc64__
#undef __sparc64__
#endif
])
])
dnl Sets the HOST_CPU_C_ABI_32BIT variable to 'yes' if the C language ABI
dnl (application binary interface) is a 32-bit one, to 'no' if it is a 64-bit
dnl one.
dnl This is a simplified variant of gl_HOST_CPU_C_ABI.
AC_DEFUN([gl_HOST_CPU_C_ABI_32BIT],
[
AC_REQUIRE([AC_CANONICAL_HOST])
AC_CACHE_CHECK([32-bit host C ABI], [gl_cv_host_cpu_c_abi_32bit],
[case "$host_cpu" in
# CPUs that only support a 32-bit ABI.
arc \
| bfin \
| cris* \
| csky \
| epiphany \
| ft32 \
| h8300 \
| m68k \
| microblaze | microblazeel \
| nds32 | nds32le | nds32be \
| nios2 | nios2eb | nios2el \
| or1k* \
| or32 \
| sh | sh[1234] | sh[1234]e[lb] \
| tic6x \
| xtensa* )
gl_cv_host_cpu_c_abi_32bit=yes
;;
# CPUs that only support a 64-bit ABI.
changequote(,)dnl
alpha | alphaev[4-8] | alphaev56 | alphapca5[67] | alphaev6[78] \
| mmix )
changequote([,])dnl
gl_cv_host_cpu_c_abi_32bit=no
;;
*)
if test -n "$gl_cv_host_cpu_c_abi"; then
dnl gl_HOST_CPU_C_ABI has already been run. Use its result.
case "$gl_cv_host_cpu_c_abi" in
i386 | x86_64-x32 | arm | armhf | arm64-ilp32 | hppa | ia64-ilp32 | mips | mipsn32 | powerpc | riscv*-ilp32* | s390 | sparc)
gl_cv_host_cpu_c_abi_32bit=yes ;;
x86_64 | alpha | arm64 | aarch64c | hppa64 | ia64 | mips64 | powerpc64 | powerpc64-elfv2 | riscv*-lp64* | s390x | sparc64 )
gl_cv_host_cpu_c_abi_32bit=no ;;
*)
gl_cv_host_cpu_c_abi_32bit=unknown ;;
esac
else
gl_cv_host_cpu_c_abi_32bit=unknown
fi
if test $gl_cv_host_cpu_c_abi_32bit = unknown; then
AC_COMPILE_IFELSE(
[AC_LANG_SOURCE(
[[int test_pointer_size[sizeof (void *) - 5];
]])],
[gl_cv_host_cpu_c_abi_32bit=no],
[gl_cv_host_cpu_c_abi_32bit=yes])
fi
;;
esac
])
HOST_CPU_C_ABI_32BIT="$gl_cv_host_cpu_c_abi_32bit"
])

View File

@ -0,0 +1,62 @@
import gi
from gi.repository import GObject
from cthulhu import resource_manager
class DynamicApiManager():
def __init__(self, app):
self.app = app
self.resourceManager = self.app.getResourceManager()
self.api = {'Cthulhu': self.app}
def registerAPI(self, key, value, application = '', contextName = None, overwrite = False):
if not overwrite:
try:
d = self.api[application][key]
return
except KeyError:
pass
# add profile
try:
d = self.api[application]
except KeyError:
self.api[application]= {}
# add dynamic API
self.api[application][key] = value
resourceContext = self.resourceManager.getResourceContext(contextName)
if resourceContext:
resourceEntry = resource_manager.ResourceEntry('api', key, value, value, key)
resourceContext.addAPI(application, key, resourceEntry)
def unregisterAPI(self, key, application = '', contextName = None):
ok = False
try:
del self.api[application][key]
ok = True
except:
print('API Key: "{}/{}" not found,'.format(application, key))
resourceContext = self.resourceManager.getResourceContext(contextName)
if resourceContext:
resourceContext.removeAPI(application, key)
return ok
def getAPI(self, key, application = '', fallback = True):
# get dynamic API
api = None
try:
api = self.api[application][key]
return api
except:
if not fallback:
print('API Key: "{}/{}" not found,'.format(application, key))
return None
# we already tried this
if application == '':
return api
try:
api = self.api[application]['']
except:
print('API Key: "{}/{}" not found,'.format(application, key))
return api

160
src/cthulhu/plugin.py Normal file
View File

@ -0,0 +1,160 @@
import os, inspect
import gi
from gi.repository import GObject
class Plugin():
#__gtype_name__ = 'BasePlugin'
def __init__(self, *args, **kwargs):
self.API = None
self.pluginInfo = None
self.moduleDir = ''
self.hidden = False
self.moduleName = ''
self.name = ''
self.version = ''
self.website = ''
self.authors = []
self.buildIn = False
self.description = ''
self.iconName = ''
self.copyright = ''
self.dependencies = False
self.helpUri = ''
self.dataDir = ''
self.translationContext = None
def setApp(self, app):
self.app = app
self.dynamicApiManager = app.getDynamicApiManager()
self.signalManager = app.getSignalManager()
def getApp(self):
return self.app
def setPluginInfo(self, pluginInfo):
self.pluginInfo = pluginInfo
self.updatePluginInfoAttributes()
def getPluginInfo(self):
return self.pluginInfo
def updatePluginInfoAttributes(self):
self.moduleDir = ''
self.hidden = False
self.moduleName = ''
self.name = ''
self.version = ''
self.website = ''
self.authors = []
self.buildIn = False
self.description = ''
self.iconName = ''
self.copyright = ''
self.dependencies = False
self.helpUri = ''
self.dataDir = ''
pluginInfo = self.getPluginInfo()
if pluginInfo == None:
return
self.moduleName = self.getApp().getPluginSystemManager().getPluginModuleName(pluginInfo)
self.name = self.getApp().getPluginSystemManager().getPluginName(pluginInfo)
self.version = self.getApp().getPluginSystemManager().getPluginVersion(pluginInfo)
self.moduleDir = self.getApp().getPluginSystemManager().getPluginModuleDir(pluginInfo)
self.buildIn = self.getApp().getPluginSystemManager().isPluginBuildIn(pluginInfo)
self.description = self.getApp().getPluginSystemManager().getPluginDescription(pluginInfo)
self.hidden = self.getApp().getPluginSystemManager().isPluginHidden(pluginInfo)
self.website = self.getApp().getPluginSystemManager().getPluginWebsite(pluginInfo)
self.authors = self.getApp().getPluginSystemManager().getPluginAuthors(pluginInfo)
self.iconName = self.getApp().getPluginSystemManager().getPluginIconName(pluginInfo)
self.copyright = self.getApp().getPluginSystemManager().getPluginCopyright(pluginInfo)
self.dependencies = self.getApp().getPluginSystemManager().getPluginDependencies(pluginInfo)
#settings = self.getApp().getPluginSystemManager().getPluginSettings(pluginInfo)
#hasDependencies = self.getApp().getPluginSystemManager().hasPluginDependency(pluginInfo)
#externalData = self.getApp().getPluginSystemManager().getPluginExternalData(pluginInfo)
self.helpUri = self.getApp().getPluginSystemManager().getPlugingetHelpUri(pluginInfo)
self.dataDir = self.getApp().getPluginSystemManager().getPluginDataDir(pluginInfo)
self.updateTranslationContext()
def updateTranslationContext(self, domain = None, localeDir = None, language = None, fallbackToCthulhuTranslation = True):
self.translationContext = None
useLocaleDir = '{}/locale/'.format(self.getModuleDir())
if localeDir:
if os.path.isdir(localeDir):
useLocaleDir = localeDir
useName = self.getModuleName()
useDomain = useName
if domain:
useDomain = domain
useLanguage = None
if language:
useLanguage = language
self.translationContext = self.getApp().getTranslationManager().initTranslation(useName, domain=useDomain, localeDir=useLocaleDir, language=useLanguage, fallbackToCthulhuTranslation=fallbackToCthulhuTranslation)
# Point _ to the translation object in the globals namespace of the caller frame
try:
callerFrame = inspect.currentframe().f_back
# Install our gettext and ngettext function to the upper frame
callerFrame.f_globals['_'] = self.translationContext.gettext
callerFrame.f_globals['ngettext'] = self.translationContext.ngettext
finally:
del callerFrame # Avoid reference problems with frames (per python docs)
def getTranslationContext(self):
return self.translationContext
def isPluginBuildIn(self):
return self.buildIn
def isPluginHidden(self):
return self.hidden
def getAuthors(self):
return self.authors
def getCopyright(self):
return self.copyright
def getDataDir(self):
return self.dataDir
def getDependencies(self):
return self.dependencies
def getDescription(self):
return self.description
def getgetHelpUri(self):
return self.helpUri
def getIconName(self):
return self.iconName
def getModuleDir(self):
return self.moduleDir
def getModuleName(self):
return self.moduleName
def getName(self):
return self.name
def getVersion(self):
return self.version
def getWebsite(self):
return self.website
def getSetting(key):
#self.getModuleName())
return None
def setSetting(key, value):
#self.getModuleName())
pass
def registerGestureByString(self, function, name, gestureString, learnModeEnabled = True):
keybinding = self.getApp().getAPIHelper().registerGestureByString(function, name, gestureString, 'default', 'cthulhu', learnModeEnabled, contextName = self.getModuleName())
return keybinding
def unregisterShortcut(self, function, name, gestureString, learnModeEnabled = True):
ok = self.getApp().getAPIHelper().unregisterShortcut(keybinding, contextName = self.getModuleName())
return ok
def registerSignal(self, signalName, signalFlag = GObject.SignalFlags.RUN_LAST, closure = GObject.TYPE_NONE, accumulator=()):
ok = self.signalManager.registerSignal(signalName, signalFlag, closure, accumulator, contextName = self.getModuleName())
return ok
def unregisterSignal(self, signalName):
# how to unregister?
pass
def connectSignal(self, signalName, function, param = None):
signalID = self.signalManager.connectSignal(signalName, function, param, contextName = self.getModuleName())
return signalID
def disconnectSignalByFunction(self, function):
# need get mapped function
mappedFunction = function
self.signalManager.disconnectSignalByFunction(mappedFunction, contextName = self.getModuleName())
def registerAPI(self, key, value, application = ''):
ok = self.dynamicApiManager.registerAPI(key, value, application = application, contextName = self.getModuleName())
return ok
def unregisterAPI(self, key, application = ''):
self.dynamicApiManager.unregisterAPI(key, application = application, contextName = self.getModuleName())

View File

@ -0,0 +1,520 @@
#!/bin/python
"""PluginManager for loading cthulhu plugins."""
import os, inspect, sys, tarfile, shutil
from enum import IntEnum
version = sys.version[:3] # we only need major.minor version.
if version in ["3.3","3.4"]:
from importlib.machinery import SourceFileLoader
else: # Python 3.5+, no support for python < 3.3.
import importlib.util
import gi
gi.require_version('Peas', '1.0')
from gi.repository import GObject
from gi.repository import Peas
gi.require_version('Atspi', '2.0')
from gi.repository import Atspi
from cthulhu import resource_manager
class API(GObject.GObject):
"""Interface that gives access to all the objects of Cthulhu."""
def __init__(self, app):
GObject.GObject.__init__(self)
self.app = app
class PluginType(IntEnum):
"""Types of plugins we support, depending on their directory location."""
# pylint: disable=comparison-with-callable,inconsistent-return-statements,no-else-return
# SYSTEM: provides system wide plugins
SYSTEM = 1
# USER: provides per user plugin
USER = 2
def __str__(self):
if self.value == PluginType.SYSTEM:
return _("System plugin")
elif self.value == PluginType.USER:
return _("User plugin")
def get_root_dir(self):
"""Returns the directory where this type of plugins can be found."""
if self.value == PluginType.SYSTEM:
return os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile(inspect.currentframe())))) + '/plugins'
elif self.value == PluginType.USER:
return os.path.expanduser('~') + '/.local/share/cthulhu/plugins'
class PluginSystemManager():
"""Orca Plugin Manager to handle a set of plugins.
Attributes:
DEFAULT_LOADERS (tuple): Default loaders used by the plugin manager. For
possible values see
https://developer.gnome.org/libpeas/stable/PeasEngine.html#peas-engine-enable-loader
"""
DEFAULT_LOADERS = ("python3", )
def __init__(self, app):
self.app = app
self.engine = Peas.Engine.get_default()
for loader in self.DEFAULT_LOADERS:
self.engine.enable_loader(loader)
self._setupPluginsDir()
self._setupExtensionSet()
if self.app:
self.gsettingsManager = self.app.getSettingsManager()
# settings else:
# settings self.gsettingsManager = gsettings_manager.getSettingsManager(self.app)
self._activePlugins = []
self._ignorePluginModulePath = []
@property
def plugins(self):
"""Gets the engine's plugin list."""
return self.engine.get_plugin_list()
@classmethod
def getPluginType(cls, pluginInfo):
"""Gets the PluginType for the specified Peas.PluginInfo."""
paths = [pluginInfo.get_data_dir(), PluginType.SYSTEM.get_root_dir()]
if os.path.commonprefix(paths) == PluginType.SYSTEM.get_root_dir():
return PluginType.SYSTEM
return PluginType.USER
def getExtension(self, pluginInfo):
if not pluginInfo:
return None
return self.extension_set.get_extension(pluginInfo)
def rescanPlugins(self):
self.engine.garbage_collect()
self.engine.rescan_plugins()
def getApp(self):
return self.app
def getPluginInfoByName(self, pluginName, pluginType=PluginType.USER):
"""Gets the plugin info for the specified plugin name.
Args:
pluginName (str): The name from the .plugin file of the module.
Returns:
Peas.PluginInfo: The plugin info if it exists. Otherwise, `None`.
"""
for pluginInfo in self.plugins:
if pluginInfo.get_module_name() == pluginName and PluginSystemManager.getPluginType(pluginInfo) == pluginType:
return pluginInfo
return None
def getActivePlugins(self):
return self._activePlugins
def setActivePlugins(self, activePlugins):
self._activePlugins = activePlugins
self.syncAllPluginsActive()
def isPluginBuildIn(self, pluginInfo):
return pluginInfo.is_builtin()
def isPluginHidden(self, pluginInfo):
return pluginInfo.is_hidden()
def getPluginAuthors(self, pluginInfo):
return pluginInfo.get_authors()
def getPluginCopyright(self, pluginInfo):
return pluginInfo.get_copyright()
def getPluginDataDir(self, pluginInfo):
return pluginInfo.get_data_dir()
def getPluginDependencies(self, pluginInfo):
return pluginInfo.get_dependencies()
def getPluginDescription(self, pluginInfo):
return pluginInfo.get_description()
def getPlugingetHelpUri(self, pluginInfo):
return pluginInfo.get_help_uri()
def getPluginIconName(self, pluginInfo):
return pluginInfo.get_icon_name()
def getPluginModuleDir(self, pluginInfo):
return pluginInfo.get_module_dir()
def getPluginModuleName(self, pluginInfo):
return pluginInfo.get_module_name()
def getPluginName(self, pluginInfo):
return pluginInfo.get_name()
def getPluginSettings(self, pluginInfo):
return pluginInfo.get_settings()
def getPluginVersion(self, pluginInfo):
return pluginInfo.get_version()
def getPluginWebsite(self, pluginInfo):
return pluginInfo.get_website()
# has_dependency and get_external_data seems broken-> takes exactly 2 arguments (1 given) but documentation doesnt say any parameter
#def hasPluginDependency(self, pluginInfo):
# return pluginInfo.has_dependency()
#def getPluginExternalData(self, pluginInfo):
# return pluginInfo.get_external_data()
def isPluginAvailable(self, pluginInfo):
try:
return pluginInfo.is_available()
except:
return False
def isPluginLoaded(self, pluginInfo):
try:
return pluginInfo.is_loaded()
except:
return False
def getIgnoredPlugins(self):
return self._ignorePluginModulePath
def setIgnoredPlugins(self, pluginModulePath, ignored):
if pluginModulePath.endswith('/'):
pluginModulePath = pluginModulePath[:-1]
if ignored:
if not pluginModulePath in self.getIgnoredPlugins():
self._ignorePluginModulePath.append(pluginModulePath)
else:
if pluginModulePath in self.getIgnoredPlugins():
self._ignorePluginModulePath.remove(pluginModulePath)
def setPluginActive(self, pluginInfo, active):
if self.isPluginBuildIn(pluginInfo):
active = True
pluginName = self.getPluginModuleName(pluginInfo)
if active:
if not pluginName in self.getActivePlugins():
if self.loadPlugin(pluginInfo):
self._activePlugins.append(pluginName )
else:
if pluginName in self.getActivePlugins():
if self.unloadPlugin(pluginInfo):
self._activePlugins.remove(pluginName )
def isPluginActive(self, pluginInfo):
if self.isPluginBuildIn(pluginInfo):
return True
if self.isPluginLoaded(pluginInfo):
return True
active_plugin_names = self.getActivePlugins()
return self.getPluginModuleName(pluginInfo) in active_plugin_names
def syncAllPluginsActive(self, ForceAllPlugins=False):
self.unloadAllPlugins(ForceAllPlugins)
self.loadAllPlugins(ForceAllPlugins)
def loadAllPlugins(self, ForceAllPlugins=False):
"""Loads plugins from settings."""
for pluginInfo in self.plugins:
if self.isPluginActive(pluginInfo) or ForceAllPlugins:
self.loadPlugin(pluginInfo)
def loadPlugin(self, pluginInfo):
resourceManager = self.getApp().getResourceManager()
moduleName = pluginInfo.get_module_name()
try:
if pluginInfo not in self.plugins:
print("Plugin missing: {}".format(moduleName))
return False
resourceManager.addResourceContext(moduleName)
self.engine.load_plugin(pluginInfo)
except Exception as e:
print('loadPlugin:',e)
return False
return True
def unloadAllPlugins(self, ForceAllPlugins=False):
"""Loads plugins from settings."""
for pluginInfo in self.plugins:
if not self.isPluginActive(pluginInfo) or ForceAllPlugins:
self.unloadPlugin(pluginInfo)
def unloadPlugin(self, pluginInfo):
resourceManager = self.getApp().getResourceManager()
moduleName = pluginInfo.get_module_name()
try:
if pluginInfo not in self.plugins:
print("Plugin missing: {}".format(moduleName))
return False
if self.isPluginBuildIn(pluginInfo):
return False
self.engine.unload_plugin(pluginInfo)
self.getApp().getResourceManager().removeResourceContext(moduleName)
self.engine.garbage_collect()
except Exception as e:
print('unloadPlugin:',e)
return False
return True
def installPlugin(self, pluginFilePath, pluginType=PluginType.USER):
if not self.isValidPluginFile(pluginFilePath):
return False
pluginFolder = pluginType.get_root_dir()
if not pluginFolder.endswith('/'):
pluginFolder += '/'
if not os.path.exists(pluginFolder):
os.mkdir(pluginFolder)
else:
if not os.path.isdir(pluginFolder):
return False
try:
with tarfile.open(pluginFilePath) as tar:
tar.extractall(path=pluginFolder)
except Exception as e:
print(e)
pluginModulePath = self.getModuleDirByPluginFile(pluginFilePath)
if pluginModulePath != '':
pluginModulePath = pluginFolder + pluginModulePath
self.setIgnoredPlugins(pluginModulePath[:-1], False) # without ending /
print('install', pluginFilePath)
self.callPackageTriggers(pluginModulePath, 'onPostInstall')
self.rescanPlugins()
return True
def getModuleDirByPluginFile(self, pluginFilePath):
if not isinstance(pluginFilePath, str):
return ''
if pluginFilePath == '':
return ''
if not os.path.exists(pluginFilePath):
return ''
try:
with tarfile.open(pluginFilePath) as tar:
tarMembers = tar.getmembers()
for tarMember in tarMembers:
if tarMember.isdir():
return tarMember.name
except Exception as e:
print(e)
return ''
def isValidPluginFile(self, pluginFilePath):
if not isinstance(pluginFilePath, str):
return False
if pluginFilePath == '':
return False
if not os.path.exists(pluginFilePath):
return False
pluginFolder = ''
pluginFileExists = False
packageFileExists = False
try:
with tarfile.open(pluginFilePath) as tar:
tarMembers = tar.getmembers()
for tarMember in tarMembers:
if tarMember.isdir():
if pluginFolder == '':
pluginFolder = tarMember.name
if tarMember.isfile():
if tarMember.name.endswith('.plugin'):
pluginFileExists = True
if tarMember.name.endswith('package.py'):
pluginFileExists = True
if not tarMember.name.startswith(pluginFolder):
return False
except Exception as e:
print(e)
return False
return pluginFileExists
def uninstallPlugin(self, pluginInfo):
if self.isPluginBuildIn(pluginInfo):
return False
# do we want to allow removing system plugins?
if PluginSystemManager.getPluginType(pluginInfo) == PluginType.SYSTEM:
return False
pluginFolder = pluginInfo.get_data_dir()
if not pluginFolder.endswith('/'):
pluginFolder += '/'
if not os.path.isdir(pluginFolder):
return False
if self.isPluginActive(pluginInfo):
self.setPluginActive(pluginInfo, False)
SettingsManager = self.app.getSettingsManager()
# TODO SettingsManager.set_settings_value_list('active-plugins', self.getActivePlugins())
self.callPackageTriggers(pluginFolder, 'onPreUninstall')
try:
shutil.rmtree(pluginFolder, ignore_errors=True)
except Exception as e:
print(e)
return False
self.setIgnoredPlugins(pluginFolder, True)
self.rescanPlugins()
return True
def callPackageTriggers(self, pluginPath, trigger):
if not os.path.exists(pluginPath):
return
if not pluginPath.endswith('/'):
pluginPath += '/'
packageModulePath = pluginPath + 'package.py'
if not os.path.isfile(packageModulePath):
return
if not os.access(packageModulePath, os.R_OK):
return
package = self.getApp().getAPIHelper().importModule('package', packageModulePath)
if trigger == 'onPostInstall':
try:
package.onPostInstall(pluginPath, self.getApp())
except Exception as e:
print(e)
elif trigger == 'onPreUninstall':
try:
package.onPreUninstall(pluginPath, self.getApp())
except Exception as e:
print(e)
def _setupExtensionSet(self):
plugin_iface = API(self.getApp())
self.extension_set = Peas.ExtensionSet.new(self.engine,
Peas.Activatable,
["object"],
[plugin_iface])
self.extension_set.connect("extension-removed",
self.__extensionRemoved)
self.extension_set.connect("extension-added",
self.__extensionAdded)
def _setupPluginsDir(self):
system_plugins_dir = PluginType.SYSTEM.get_root_dir()
user_plugins_dir = PluginType.USER.get_root_dir()
if os.path.exists(user_plugins_dir):
self.engine.add_search_path(user_plugins_dir)
if os.path.exists(system_plugins_dir):
self.engine.add_search_path(system_plugins_dir)
def __extensionRemoved(self, unusedSet, pluginInfo, extension):
extension.deactivate()
def __extensionAdded(self, unusedSet, pluginInfo, extension):
extension.setApp(self.getApp())
extension.setPluginInfo(pluginInfo)
extension.activate()
def __loadedPlugins(self, engine, unusedSet):
"""Handles the changing of the loaded plugin list."""
self.getApp().settings.ActivePlugins = engine.get_property("loaded-plugins")
class APIHelper():
def __init__(self, app):
self.app = app
self.cthulhuKeyBindings = None
'''
_pluginAPIManager.seCthulhuAPI('Logger', _logger)
_pluginAPIManager.setCthulhuAPI('SettingsManager', _settingsManager)
_pluginAPIManager.setCthulhuAPI('ScriptManager', _scriptManager)
_pluginAPIManager.setCthulhuAPI('EventManager', _eventManager)
_pluginAPIManager.setCthulhuAPI('Speech', speech)
_pluginAPIManager.setCthulhuAPI('Sound', sound)
_pluginAPIManager.setCthulhuAPI('Braille', braille)
_pluginAPIManager.setCthulhuAPI('Debug', debug)
_pluginAPIManager.setCthulhuAPI('Messages', messages)
_pluginAPIManager.setCthulhuAPI('MouseReview', mouse_review)
_pluginAPIManager.setCthulhuAPI('NotificationMessages', notification_messages)
_pluginAPIManager.setCthulhuAPI('CthulhuState', cthulhu_state)
_pluginAPIManager.setCthulhuAPI('CthulhuPlatform', cthulhu_platform)
_pluginAPIManager.setCthulhuAPI('Settings', settings)
_pluginAPIManager.setCthulhuAPI('Keybindings', keybindings)
'''
def outputMessage(self, Message, interrupt=False):
settings = self.app.getDynamicApiManager().getAPI('Settings')
braille = self.app.getDynamicApiManager().getAPI('Braille')
speech = self.app.getDynamicApiManager().getAPI('Speech')
if speech != None:
if (settings.enableSpeech):
if interrupt:
speech.cancel()
if Message != '':
speech.speak(Message)
if braille != None:
if (settings.enableBraille):
braille.displayMessage(Message)
def createInputEventHandler(self, function, name, learnModeEnabled=True):
EventManager = self.app.getDynamicApiManager().getAPI('EventManager')
newInputEventHandler = EventManager.input_event.InputEventHandler(function, name, learnModeEnabled)
return newInputEventHandler
def registerGestureByString(self, function, name, gestureString, profile, application, learnModeEnabled = True, contextName = None):
gestureList = gestureString.split(',')
registeredGestures = []
for gesture in gestureList:
if gesture.startswith('kb:'):
shortcutString = gesture[3:]
registuredGesture = self.registerShortcutByString(function, name, shortcutString, profile, application, learnModeEnabled, contextName=contextName)
if registuredGesture:
registeredGestures.append(registuredGesture)
return registeredGestures
def registerShortcutByString(self, function, name, shortcutString, profile, application, learnModeEnabled = True, contextName = None):
keybindings = self.app.getDynamicApiManager().getAPI('Keybindings')
settings = self.app.getDynamicApiManager().getAPI('Settings')
resourceManager = self.app.getResourceManager()
clickCount = 0
cthulhuKey = False
shiftKey = False
ctrlKey = False
altKey = False
key = ''
shortcutList = shortcutString.split('+')
for shortcutElement in shortcutList:
shortcutElementLower = shortcutElement.lower()
if shortcutElementLower == 'press':
clickCount += 1
elif shortcutElement == 'cthulhu':
cthulhuKey = True
elif shortcutElementLower == 'shift':
shiftKey = True
elif shortcutElementLower == 'control':
ctrlKey = True
elif shortcutElementLower == 'alt':
altKey = True
else:
key = shortcutElementLower
if clickCount == 0:
clickCount = 1
if self.cthulhuKeyBindings == None:
self.cthulhuKeyBindings = keybindings.KeyBindings()
tryFunction = resource_manager.TryFunction(function)
newInputEventHandler = self.createInputEventHandler(tryFunction.runInputEvent, name, learnModeEnabled)
currModifierMask = keybindings.NO_MODIFIER_MASK
if cthulhuKey:
currModifierMask = currModifierMask | 1 << keybindings.MODIFIER_ORCA
if shiftKey:
currModifierMask = currModifierMask | 1 << Atspi.ModifierType.SHIFT
if altKey:
currModifierMask = currModifierMask | 1 << Atspi.ModifierType.ALT
if ctrlKey:
currModifierMask = currModifierMask | 1 << Atspi.ModifierType.CONTROL
newKeyBinding = keybindings.KeyBinding(key, keybindings.defaultModifierMask, currModifierMask, newInputEventHandler, clickCount)
self.cthulhuKeyBindings.add(newKeyBinding)
settings.keyBindingsMap["default"] = self.cthulhuKeyBindings
if contextName:
resourceContext = resourceManager.getResourceContext(contextName)
if resourceContext:
resourceEntry = resource_manager.ResourceEntry('keyboard', newKeyBinding, function, tryFunction, shortcutString)
resourceContext.addGesture(profile, application, newKeyBinding, resourceEntry)
return newKeyBinding
def unregisterShortcut(self, KeyBindingToRemove, contextName = None):
ok = False
keybindings = self.app.getDynamicApiManager().getAPI('Keybindings')
settings = self.app.getDynamicApiManager().getAPI('Settings')
resourceManager = self.app.getResourceManager()
if self.cthulhuKeyBindings == None:
self.cthulhuKeyBindings = keybindings.KeyBindings()
try:
self.cthulhuKeyBindings.remove(KeyBindingToRemove)
settings.keyBindingsMap["default"] = self.cthulhuKeyBindings
ok = True
except KeyError:
pass
if contextName:
resourceContext = resourceManager.getResourceContext(contextName)
if resourceContext:
resourceContext.removeGesture(KeyBindingToRemove)
return ok
def importModule(self, moduleName, moduleLocation):
if version in ["3.3","3.4"]:
return SourceFileLoader(moduleName, moduleLocation).load_module()
else:
spec = importlib.util.spec_from_file_location(moduleName, moduleLocation)
driver_mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(driver_mod)
return driver_mod

View File

@ -0,0 +1,6 @@
[Plugin]
Module=ByeCthulhu
Loader=python3
Name=Stop announcement for cthulhu
Description=Test plugin for cthulhu
Authors=Chrys chrys@linux-a11y.org

View File

@ -0,0 +1,27 @@
from cthulhu import plugin
import gi
gi.require_version('Peas', '1.0')
from gi.repository import GObject
from gi.repository import Peas
import time
class ByeOrca(GObject.Object, Peas.Activatable, plugin.Plugin):
#__gtype_name__ = 'ByeCthulhu'
object = GObject.Property(type=GObject.Object)
def __init__(self):
plugin.Plugin.__init__(self)
def do_activate(self):
API = self.object
self.connectSignal("stop-application-completed", self.process)
def do_deactivate(self):
API = self.object
def do_update_state(self):
API = self.object
def process(self, app):
messages = app.getDynamicApiManager().getAPI('Messages')
activeScript = app.getDynamicApiManager().getAPI('CthulhuState').activeScript
activeScript.presentationInterrupt()
activeScript.presentMessage(messages.STOP_ORCA, resetStyles=False)

View File

@ -0,0 +1,7 @@
orca_python_PYTHON = \
__init__.py \
ByeCthulhu.plugin \
ByeCthulhu.py
orca_pythondir=$(pkgpythondir)/plugins/ByeCthulhu

View File

@ -0,0 +1,6 @@
[Plugin]
Module=CapsLockHack
Loader=python3
Name=Caps Lock Hack
Description=Fix Capslock sometimes switch on / off when its used as modifier
Authors=Chrys chrys@linux-a11y.org

View File

@ -0,0 +1,119 @@
from cthulhu import plugin
import gi
gi.require_version('Peas', '1.0')
from gi.repository import GObject
from gi.repository import Peas
from threading import Thread, Lock
import subprocess, time, re, os
class CapsLockHack(GObject.Object, Peas.Activatable, plugin.Plugin):
__gtype_name__ = 'CapsLockHack'
object = GObject.Property(type=GObject.Object)
def __init__(self):
plugin.Plugin.__init__(self)
self.lock = Lock()
self.active = False
self.workerThread = Thread(target=self.worker)
def do_activate(self):
API = self.object
"""Enable or disable use of the caps lock key as an Cthulhu modifier key."""
self.interpretCapsLineProg = re.compile(
r'^\s*interpret\s+Caps[_+]Lock[_+]AnyOfOrNone\s*\(all\)\s*{\s*$', re.I)
self.normalCapsLineProg = re.compile(
r'^\s*action\s*=\s*LockMods\s*\(\s*modifiers\s*=\s*Lock\s*\)\s*;\s*$', re.I)
self.interpretShiftLineProg = re.compile(
r'^\s*interpret\s+Shift[_+]Lock[_+]AnyOf\s*\(\s*Shift\s*\+\s*Lock\s*\)\s*{\s*$', re.I)
self.normalShiftLineProg = re.compile(
r'^\s*action\s*=\s*LockMods\s*\(\s*modifiers\s*=\s*Shift\s*\)\s*;\s*$', re.I)
self.disabledModLineProg = re.compile(
r'^\s*action\s*=\s*NoAction\s*\(\s*\)\s*;\s*$', re.I)
self.normalCapsLine = ' action= LockMods(modifiers=Lock);'
self.normalShiftLine = ' action= LockMods(modifiers=Shift);'
self.disabledModLine = ' action= NoAction();'
self.activateWorker()
def do_deactivate(self):
API = self.object
self.deactivateWorker()
def do_update_state(self):
API = self.object
def deactivateWorker(self):
with self.lock:
self.active = False
self.workerThread.join()
def activateWorker(self):
with self.lock:
self.active = True
self.workerThread.start()
def isActive(self):
with self.lock:
return self.active
def worker(self):
"""Makes an Cthulhu-specific Xmodmap so that the keys behave as we
need them to do. This is especially the case for the Cthulhu modifier.
"""
API = self.object
capsLockCleared = False
settings = API.app.getDynamicApiManager().getAPI('Settings')
time.sleep(3)
while self.isActive():
if "Caps_Lock" in settings.orcaModifierKeys \
or "Shift_Lock" in settings.orcaModifierKeys:
self.setCapsLockAsOrcaModifier(True)
capsLockCleared = True
elif capsLockCleared:
self.setCapsLockAsOrcaModifier(False)
capsLockCleared = False
time.sleep(1)
def setCapsLockAsOrcaModifier(self, enable):
originalXmodmap = None
lines = None
try:
originalXmodmap = subprocess.check_output(['xkbcomp', os.environ['DISPLAY'], '-'])
lines = originalXmodmap.decode('UTF-8').split('\n')
except:
return
foundCapsInterpretSection = False
foundShiftInterpretSection = False
modified = False
for i, line in enumerate(lines):
if not foundCapsInterpretSection and not foundShiftInterpretSection:
if self.interpretCapsLineProg.match(line):
foundCapsInterpretSection = True
elif self.interpretShiftLineProg.match(line):
foundShiftInterpretSection = True
elif foundCapsInterpretSection:
if enable:
if self.normalCapsLineProg.match(line):
lines[i] = self.disabledModLine
modified = True
else:
if self.disabledModLineProg.match(line):
lines[i] = self.normalCapsLine
modified = True
if line.find('}'):
foundCapsInterpretSection = False
else: # foundShiftInterpretSection
if enable:
if self.normalShiftLineProg.match(line):
lines[i] = self.disabledModLine
modified = True
else:
if self.disabledModLineProg.match(line):
lines[i] = self.normalShiftLine
modified = True
if line.find('}'):
foundShiftInterpretSection = False
if modified:
newXmodMap = bytes('\n'.join(lines), 'UTF-8')
self.setXmodmap(newXmodMap)
def setXmodmap(self, xkbmap):
"""Set the keyboard map using xkbcomp."""
try:
p = subprocess.Popen(['xkbcomp', '-w0', '-', os.environ['DISPLAY']],
stdin=subprocess.PIPE, stdout=None, stderr=None)
p.communicate(xkbmap)
except:
pass