15 Commits

15 changed files with 456 additions and 5 deletions

View File

@ -29,6 +29,8 @@ Cthulhu has the following dependencies:
* liblouis - Liblouis (<http://liblouis.org/>) support for contracted braille (optional)
* py-setproctitle - Python library to set the process title (optional)
* gstreamer-1.0 - GStreamer - Streaming media framework (optional)
* socat - Used for self-voicing functionality.
* libpeas - For the plugin system.
You are strongly encouraged to also have the latest stable versions
of AT-SPI2 and ATK.

1
TODO
View File

@ -1,4 +1,3 @@
- Add Simple Cthulhu Plugin System as native code for Cthulhu
- Merge in sleep mode.
- Add in ocrdesktop code so that Cthulhu has native ocr support.
- Get rid of build systems. My hope is git clone and run so long as requirements are satisfied.

View File

@ -1,4 +1,4 @@
m4_define([cthulhu_version], [0.3])
m4_define([cthulhu_version], [0.4])
m4_define(pygobject_required_version, 3.18)
m4_define(atspi_required_version, 2.48)
@ -28,6 +28,7 @@ PKG_CHECK_MODULES([PYGOBJECT], [pygobject-3.0 >= pygobject_required_version])
PKG_CHECK_MODULES([ATSPI2], [atspi-2 >= atspi_required_version])
PKG_CHECK_MODULES([ATKBRIDGE], [atk-bridge-2.0 >= atkbridge_required_version])
PKG_CHECK_MODULES([GSTREAMER], [gstreamer-1.0], [gstreamer="yes"], [gstreamer="no"])
PKG_CHECK_MODULES([LIBPEAS], [libpeas-1.0], [libpeas="yes"], [libpeas="no"])
dnl Needed programs
AC_PROG_INSTALL
@ -130,6 +131,7 @@ src/cthulhu/plugins/Date/Makefile
src/cthulhu/plugins/Time/Makefile
src/cthulhu/plugins/MouseReview/Makefile
src/cthulhu/plugins/ClassicPreferences/Makefile
src/cthulhu/plugins/SimplePluginSystem/Makefile
src/cthulhu/backends/Makefile
src/cthulhu/cthulhu_bin.py
src/cthulhu/cthulhu_i18n.py
@ -157,6 +159,10 @@ echo
echo "NOTE: Sound support requires gstreamer-1.0."
fi
if test "$have_libpeas" = "no"; then
AC_MSG_ERROR([libpeas-1.0 >= 1.20 is required])
fi
echo
echo Use speech-dispatcher: $speechd_available
echo Use brltty: $brlapi_available

View File

@ -1,7 +1,7 @@
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
pkgname=cthulhu
pkgver=0.3
pkgver=0.4
pkgrel=1
pkgdesc="Screen reader for individuals who are blind or visually impaired forked from Orca"
url="https://git.stormux.org/storm/cthulhu"

View File

@ -0,0 +1,28 @@
Cthulhu is a screen reader for individuals who are blind or visually impaired,
forked from Orca. It provides access to applications and toolkits that support
the AT-SPI (e.g., the GNOME desktop).
This screen reader helps users navigate their desktop environment and applications
through speech synthesis and braille output.
After installation, you can start Cthulhu through the GNOME desktop environment
or by running 'cthulhu' from the command line.
DEPENDENCIES:
This package requires the following packages, all available from SlackBuilds.org:
- at-spi2-core
- brltty
- gobject-introspection
- gsettings-desktop-schemas
- gstreamer
- gst-plugins-base
- gst-plugins-good
- gtk3
- liblouis
- libpeas
- libwnck3
- python3-atspi
- python3-cairo
- python3-gobject
- python3-setproctitle
- speech-dispatcher

View File

@ -0,0 +1,10 @@
PRGNAM="cthulhu"
VERSION="0.4"
HOMEPAGE="https://git.stormux.org/storm/cthulhu"
DOWNLOAD="https://git.stormux.org/storm/cthulhu.git"
MD5SUM="SKIP"
DOWNLOAD_x86_64=""
MD5SUM_x86_64=""
REQUIRES="at-spi2-core brltty gobject-introspection gsettings-desktop-schemas gstreamer gst-plugins-base gst-plugins-good gtk3 liblouis libpeas libwnck3 python3-atspi python3-cairo python3-gobject python3-setproctitle speech-dispatcher"
MAINTAINER="Storm Dragon"
EMAIL="storm_dragon@stormux.org"

View File

@ -0,0 +1,94 @@
#!/bin/bash
# Slackware build script for cthulhu
# Created based on PKGBUILD from Storm Dragon <storm_dragon@stormux.org>
cd $(dirname $0) ; CWD=$(pwd)
PRGNAM=cthulhu
VERSION=${VERSION:-0.4}
BUILD=${BUILD:-1}
TAG=${TAG:-_SBo}
PKGTYPE=${PKGTYPE:-tgz}
if [ -z "$ARCH" ]; then
case "$( uname -m )" in
i?86) ARCH=i586 ;;
arm*) ARCH=arm ;;
*) ARCH=$( uname -m ) ;;
esac
fi
# If the variable PRINT_PACKAGE_NAME is set, then this script will report what
# the name of the created package would be, and then exit. This information
# could be useful to other scripts.
if [ ! -z "${PRINT_PACKAGE_NAME}" ]; then
echo "$PRGNAM-$VERSION-$ARCH-$BUILD$TAG.$PKGTYPE"
exit 0
fi
TMP=${TMP:-/tmp/SBo}
PKG=$TMP/package-$PRGNAM
OUTPUT=${OUTPUT:-/tmp}
if [ "$ARCH" = "i586" ]; then
SLKCFLAGS="-O2 -march=i586 -mtune=i686"
LIBDIRSUFFIX=""
elif [ "$ARCH" = "i686" ]; then
SLKCFLAGS="-O2 -march=i686 -mtune=i686"
LIBDIRSUFFIX=""
elif [ "$ARCH" = "x86_64" ]; then
SLKCFLAGS="-O2 -fPIC"
LIBDIRSUFFIX="64"
else
SLKCFLAGS="-O2"
LIBDIRSUFFIX=""
fi
set -e
rm -rf $PKG
mkdir -p $TMP $PKG $OUTPUT
cd $TMP
rm -rf $PRGNAM-$VERSION
git clone https://git.stormux.org/storm/cthulhu.git $PRGNAM-$VERSION
cd $PRGNAM-$VERSION
chown -R root:root .
find -L . \
\( -perm 777 -o -perm 775 -o -perm 750 -o -perm 711 -o -perm 555 \
-o -perm 511 \) -exec chmod 755 {} \; -o \
\( -perm 666 -o -perm 664 -o -perm 640 -o -perm 600 -o -perm 444 \
-o -perm 440 -o -perm 400 \) -exec chmod 644 {} \;
# Prepare the source
NOCONFIGURE=1 ./autogen.sh
CFLAGS="$SLKCFLAGS" \
CXXFLAGS="$SLKCFLAGS" \
./configure \
--prefix=/usr \
--libdir=/usr/lib${LIBDIRSUFFIX} \
--sysconfdir=/etc \
--localstatedir=/var \
--mandir=/usr/man \
--docdir=/usr/doc/$PRGNAM-$VERSION \
--build=$ARCH-slackware-linux
make
make install DESTDIR=$PKG
find $PKG -print0 | xargs -0 file | grep -e "executable" -e "shared object" | grep ELF \
| cut -f 1 -d : | xargs strip --strip-unneeded 2> /dev/null || true
mkdir -p $PKG/usr/doc/$PRGNAM-$VERSION
cp -a AUTHORS COPYING ChangeLog NEWS README \
$PKG/usr/doc/$PRGNAM-$VERSION
cat $CWD/$PRGNAM.SlackBuild > $PKG/usr/doc/$PRGNAM-$VERSION/$PRGNAM.SlackBuild
mkdir -p $PKG/install
cat $CWD/slack-desc > $PKG/install/slack-desc
cat $CWD/doinst.sh > $PKG/install/doinst.sh
cd $PKG
/sbin/makepkg -l y -c n $OUTPUT/$PRGNAM-$VERSION-$ARCH-$BUILD$TAG.$PKGTYPE

View File

@ -0,0 +1,9 @@
if [ -x /usr/bin/update-desktop-database ]; then
/usr/bin/update-desktop-database -q usr/share/applications >/dev/null 2>&1
fi
if [ -e usr/share/icons/hicolor/icon-theme.cache ]; then
if [ -x /usr/bin/gtk-update-icon-cache ]; then
/usr/bin/gtk-update-icon-cache usr/share/icons/hicolor >/dev/null 2>&1
fi
fi

View File

@ -0,0 +1,19 @@
# HOW TO EDIT THIS FILE:
# The "handy ruler" below makes it easier to edit a package description.
# Line up the first '|' above the ':' following the base package name, and
# the '|' on the right side marks the last column you can put a character in.
# You must make exactly 11 lines for the formatting to be correct. It's also
# customary to leave one space after the ':' except on otherwise blank lines.
|-----handy-ruler------------------------------------------------------|
cthulhu: cthulhu (Screen reader for blind or visually impaired users)
cthulhu:
cthulhu: Cthulhu is a screen reader for individuals who are blind or visually
cthulhu: impaired, forked from Orca. It provides a way to access applications
cthulhu: and toolkits that support the AT-SPI (e.g., the GNOME desktop).
cthulhu:
cthulhu: Homepage: https://git.stormux.org/storm/cthulhu
cthulhu:
cthulhu:
cthulhu:
cthulhu:

View File

@ -1,4 +1,4 @@
SUBDIRS = Clipboard HelloWorld SelfVoice Time MouseReview Date ByeCthulhu HelloCthulhu PluginManager CapsLockHack ClassicPreferences
SUBDIRS = Clipboard HelloWorld SelfVoice Time MouseReview Date ByeCthulhu HelloCthulhu PluginManager CapsLockHack ClassicPreferences SimplePluginSystem
cthulhu_pythondir=$(pkgpythondir)/plugins

View File

@ -0,0 +1,7 @@
cthulhu_python_PYTHON = \
__init__.py \
SimplePluginSystem.plugin \
SimplePluginSystem.py
cthulhu_pythondir=$(pkgpythondir)/plugins/SimplePluginSystem

View File

@ -0,0 +1,11 @@
[Plugin]
Module=SimplePluginSystem
Loader=python3
Name=Simple Plugin System
Description=Simple plugin system implementation for Cthulhu
Authors=Chrys <chrys@linux-a11y.org>;Storm Dragon <storm_dragon@stormux.org>
Copyright=Copyright Â2024 Chrys, Storm Dragon
Website=https://git.stormux.org/storm/cthulhu
Version=1.0
Builtin=true

View File

@ -0,0 +1,266 @@
from cthulhu import plugin
from gi.repository import GObject, Peas
import glob
import os
import importlib.util
import random
import string
import _thread
from subprocess import Popen, PIPE
settings = None
speech = None
braille = None
input_event = None
def outputMessage( Message):
if (settings.enableSpeech):
speech.speak(Message)
if (settings.enableBraille):
braille.displayMessage(Message)
class SimplePluginSystem(GObject.Object, Peas.Activatable, plugin.Plugin):
__gtype_name__ = 'SimplePluginSystem'
object = GObject.Property(type=GObject.Object)
def __init__(self):
plugin.Plugin.__init__(self)
self.plugin_list = []
self.loaded = False
self.plugin_repo = os.path.expanduser('~') + "/.local/share/cthulhu/simple-plugins-enabled/"
def do_activate(self):
API = self.object
global settings
global speech
global braille
global input_event
settings = API.app.getDynamicApiManager().getAPI('Settings')
speech = API.app.getDynamicApiManager().getAPI('Speech')
braille = API.app.getDynamicApiManager().getAPI('Braille')
input_event = API.app.getDynamicApiManager().getAPI('InputEvent')
"""Required method for plugins"""
if not self.loaded:
self.load_plugins()
def do_deactivate(self):
"""Required method for plugins"""
# Remove all registered keybindings
for plugin in self.plugin_list:
self.unregisterShortcut(plugin['function'], plugin['shortcut'])
self.loaded = False
self.plugin_list = []
def SetupShortcutAndHandle(self, currPluginSetting):
shortcut = ''
# just the modifier
if not currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
shortcut = 'kb:cthulhu+' + currPluginSetting['key']
# cthulhu + alt
if not currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
shortcut = 'kb:cthulhu+alt+' + currPluginSetting['key']
# cthulhu + CTRL
if not currPluginSetting['shiftkey'] and currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
shortcut = 'kb:cthulhu+control+' + currPluginSetting['key']
# cthulhu + alt + CTRL
if not currPluginSetting['shiftkey'] and currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
shortcut = 'kb:cthulhu+alt+control+ ' + currPluginSetting['key']
# cthulhu + shift
if currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
shortcut = 'kb:cthulhu+shift+' + currPluginSetting['key']
# alt + shift
if currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
shortcut = 'kb:alt+shift+' + currPluginSetting['key']
if shortcut != '':
print(shortcut)
currPluginSetting['shortcut'] = shortcut
self.registerGestureByString(currPluginSetting['function'], _(currPluginSetting['pluginname']), shortcut)
return currPluginSetting
def id_generator(self, size=7, chars=string.ascii_letters):
return ''.join(random.choice(chars) for _ in range(size))
def initSettings(self):
currPluginSetting={
'pluginname':'',
'functionname':'',
'key':'',
'shiftkey':False,
'ctrlkey':False,
'altkey':False,
'startnotify':False,
'stopnotify':False,
'blockcall':False,
'error':False,
'exec': False,
'parameters':'',
'function':None,
'inputeventhandler':None,
'valid':False,
'supressoutput':False,
'shortcut': ''
}
return currPluginSetting
def getPluginSettings(self, filepath, currPluginSetting):
try:
currPluginSetting['file'] = filepath
fileName, fileExtension = os.path.splitext(filepath)
if (fileExtension and (fileExtension != '')): #if there is an extension
currPluginSetting['loadable'] = (fileExtension.lower() == '.py') # only python is loadable
filename = os.path.basename(filepath) #filename
filename = os.path.splitext(filename)[0] #remove extension if we have one
#remove pluginname seperated by __-__
filenamehelper = filename.split('__-__')
filename = filenamehelper[len(filenamehelper) - 1 ]
currPluginSetting['permission'] = os.access(filepath, os.X_OK )
currPluginSetting['pluginname'] = 'NoNameAvailable'
if len(filenamehelper) == 2:
currPluginSetting['pluginname'] = filenamehelper[0]
#now get shortcuts seperated by __+__
filenamehelper = filename.split('__+__')
if len([y for y in filenamehelper if 'parameters_' in y.lower()]) == 1 and\
len([y for y in filenamehelper if 'parameters_' in y.lower()][0]) > 11:
currPluginSetting['parameters'] = [y for y in filenamehelper if 'parameters_' in y.lower()][0][11:]
if len([y for y in filenamehelper if 'key_' in y.lower()]) == 1 and\
len([y for y in filenamehelper if 'key_' in y.lower()][0]) > 4 :
currPluginSetting['key'] = [y for y in filenamehelper if 'key_' in y.lower()][0][4]
if currPluginSetting['key'] == '':
settcurrPluginSetting = 'shift' in map(str.lower, filenamehelper)
currPluginSetting['ctrlkey'] = 'control' in map(str.lower, filenamehelper)
currPluginSetting['altkey'] = 'alt' in map(str.lower, filenamehelper)
currPluginSetting['startnotify'] = 'startnotify' in map(str.lower, filenamehelper)
currPluginSetting['stopnotify'] = 'stopnotify' in map(str.lower, filenamehelper)
currPluginSetting['blockcall'] = 'blockcall' in map(str.lower, filenamehelper)
currPluginSetting['error'] = 'error' in map(str.lower, filenamehelper)
currPluginSetting['supressoutput'] = 'supressoutput' in map(str.lower, filenamehelper)
currPluginSetting['exec'] = 'exec' in map(str.lower, filenamehelper)
currPluginSetting['loadmodule'] = 'loadmodule' in map(str.lower, filenamehelper)
currPluginSetting = self.readSettingsFromPlugin(currPluginSetting)
if not currPluginSetting['loadmodule']:
if not currPluginSetting['permission']: #subprocessing only works with exec permission
return self.initSettings()
if currPluginSetting['loadmodule'] and not currPluginSetting['loadable']: #sorry.. its not loadable only .py is loadable
return self.initSettings()
if (len(currPluginSetting['key']) > 1): #no shortcut
if not currPluginSetting['exec']: # and no exec -> the plugin make no sense because it isnt hooked anywhere
return self.initSettings() #so not load it (sets valid = False)
else:
currPluginSetting['key'] = '' #there is a strange key, but exec? ignore the key..
currPluginSetting['valid'] = True # we could load everything
return currPluginSetting
except:
return self.initSettings()
def readSettingsFromPlugin(self, currPluginSetting):
if not os.access(currPluginSetting['file'], os.R_OK ):
return currPluginSetting
fileName, fileExtension = os.path.splitext(currPluginSetting['file'])
if (fileExtension and (fileExtension != '')): #if there is an extension
if (fileExtension.lower() != '.py') and \
(fileExtension.lower() != '.sh'):
return currPluginSetting
else:
return currPluginSetting
with open(currPluginSetting['file'], "r") as pluginFile:
for line in pluginFile:
currPluginSetting['shiftkey'] = ('sopsproperty:shift' in line.lower().replace(" ", "")) or currPluginSetting['shiftkey']
currPluginSetting['ctrlkey'] = ('sopsproperty:control' in line.lower().replace(" ", "")) or currPluginSetting['ctrlkey']
currPluginSetting['altkey'] = ('sopsproperty:alt' in line.lower().replace(" ", "")) or currPluginSetting['altkey']
currPluginSetting['startnotify'] = ('sopsproperty:startnotify' in line.lower().replace(" ", "")) or currPluginSetting['startnotify']
currPluginSetting['stopnotify'] = ('sopsproperty:stopnotify' in line.lower().replace(" ", "")) or currPluginSetting['stopnotify']
currPluginSetting['blockcall'] = ('sopsproperty:blockcall' in line.lower().replace(" ", "")) or currPluginSetting['blockcall']
currPluginSetting['error'] = ('sopsproperty:error' in line.lower().replace(" ", "")) or currPluginSetting['error']
currPluginSetting['supressoutput'] = ('sopsproperty:supressoutput' in line.lower().replace(" ", "")) or currPluginSetting['supressoutput']
currPluginSetting['exec'] = ('sopsproperty:exec' in line.lower().replace(" ", "")) or currPluginSetting['exec']
currPluginSetting['loadmodule'] = ('sopsproperty:loadmodule' in line.lower().replace(" ", "")) or currPluginSetting['loadmodule']
return currPluginSetting
def buildPluginSubprocess(self, currPluginSetting):
currplugin = "\'\"" + currPluginSetting['file'] + "\" " + currPluginSetting['parameters'] + "\'"
pluginname = currPluginSetting['pluginname']
if currPluginSetting['blockcall']:
pluginname = "blocking " + pluginname
fun_body = "global " + currPluginSetting['functionname']+"\n"
fun_body += "def " + currPluginSetting['functionname'] + "(script=None, inputEvent=None):\n"
if currPluginSetting['startnotify']:
fun_body +=" outputMessage('start " + pluginname + "')\n"
fun_body +=" p = Popen(" + currplugin + ", stdout=PIPE, stderr=PIPE, shell=True)\n"
fun_body +=" stdout, stderr = p.communicate()\n"
fun_body +=" message = ''\n"
fun_body +=" if not " + str(currPluginSetting['supressoutput']) + " and stdout:\n"
fun_body +=" message += str(stdout, \"utf-8\")\n"
fun_body +=" if " + str(currPluginSetting['error']) + " and stderr:\n"
fun_body +=" message += ' error: ' + str(stderr, \"utf-8\")\n"
fun_body +=" outputMessage( message)\n"
if currPluginSetting['stopnotify']:
fun_body +=" outputMessage('finish " + pluginname + "')\n"
fun_body +=" return True\n\n"
fun_body += "global " + currPluginSetting['functionname']+"T\n"
fun_body +="def " + currPluginSetting['functionname'] + "T(script=None, inputEvent=None):\n"
fun_body +=" _thread.start_new_thread("+ currPluginSetting['functionname'] + ",(script, inputEvent))\n\n"
return fun_body
def buildPluginExec(self, currPluginSetting):
pluginname = currPluginSetting['pluginname']
if currPluginSetting['blockcall']:
pluginname = "blocking " + pluginname
fun_body = "global " + currPluginSetting['functionname']+"\n"
fun_body += "def " + currPluginSetting['functionname'] + "(script=None, inputEvent=None):\n"
if currPluginSetting['startnotify']:
fun_body +=" outputMessage('start " + pluginname + "')\n"
fun_body += " try:\n"
fun_body += " spec = importlib.util.spec_from_file_location(\"" + currPluginSetting['functionname'] + "\",\""+ currPluginSetting['file']+"\")\n"
fun_body += " "+currPluginSetting['functionname'] + "Module = importlib.util.module_from_spec(spec)\n"
fun_body += " spec.loader.exec_module(" + currPluginSetting['functionname'] + "Module)\n"
fun_body += " except:\n"
fun_body += " pass\n"
if currPluginSetting['error']:
fun_body += " outputMessage(\"Error while executing " + pluginname + "\")\n"
if currPluginSetting['stopnotify']:
fun_body +=" outputMessage('finish " + pluginname + "')\n"
fun_body += " return True\n\n"
fun_body += "global " + currPluginSetting['functionname']+"T\n"
fun_body +="def " + currPluginSetting['functionname'] + "T(script=None, inputEvent=None):\n"
fun_body +=" _thread.start_new_thread("+ currPluginSetting['functionname'] + ",(script, inputEvent))\n\n"
return fun_body
def getFunctionName(self, currPluginSetting):
currPluginSetting['functionname'] = ''
while currPluginSetting['functionname'] == '' or currPluginSetting['functionname'] + 'T' in globals() or currPluginSetting['functionname'] in globals():
currPluginSetting['functionname'] = self.id_generator()
return currPluginSetting
def load_plugins(self):
if not self.loaded:
self.plugin_list = glob.glob(self.plugin_repo+'*')
for currplugin in self.plugin_list:
currPluginSetting = self.initSettings()
currPluginSetting = self.getPluginSettings(currplugin, currPluginSetting)
if not currPluginSetting['valid']:
continue
currPluginSetting = self.getFunctionName(currPluginSetting)
if currPluginSetting['loadmodule']:
exec(self.buildPluginExec(currPluginSetting)) # load as python module
else:
exec(self.buildPluginSubprocess(currPluginSetting)) # run as subprocess
if currPluginSetting['blockcall']:
currPluginSetting['function'] = globals()[currPluginSetting['functionname']] # non threaded
else:
currPluginSetting['function'] = globals()[currPluginSetting['functionname']+"T"] # T = Threaded
if currPluginSetting['exec']: # exec on load if we want
currPluginSetting['function']()
if not currPluginSetting['key'] == '':
currPluginSetting = self.SetupShortcutAndHandle(currPluginSetting)
print(currPluginSetting)
self.plugin_list.append(currPluginSetting) # store in a list
self.loaded = True

View File

@ -407,4 +407,4 @@ presentChatRoomLast = False
presentLiveRegionFromInactiveTab = False
# Plugins
activePlugins = ['Clipboard', 'MouseReview', 'Date', 'ByeCthulhu', 'Time', 'HelloCthulhu', 'HelloWorld', 'SelfVoice', 'PluginManager', 'ClassicPreferences']
activePlugins = ['Clipboard', 'MouseReview', 'Date', 'ByeCthulhu', 'Time', 'HelloCthulhu', 'HelloWorld', 'SelfVoice', 'PluginManager', 'ClassicPreferences', 'SimplePluginSystem']