1
0
mirror of https://github.com/fcwu/docker-ubuntu-vnc-desktop synced 2026-08-05 16:12:41 +02:00

Merge branch 'develop' into bionic-lxqt

This commit is contained in:
Doro Wu
2020-04-15 08:40:32 +08:00
66 changed files with 2211 additions and 837 deletions
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
if [ -z "$ALSADEV" ]; then
zenity --error --text "To support audio, please read README.md and run container with --device /dev/snd -e ALSADEV=..."
exit 1
fi
exec /usr/bin/chromium-browser --no-sandbox --alsa-output-device="$ALSADEV" "$@"
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
exec /usr/bin/Xvfb :1 -screen 0 1024x768x24
@@ -0,0 +1,11 @@
class Default(object):
DEBUG = True
class Development(Default):
PHASE = 'development'
class Production(Default):
PHASE = 'production'
DEBUG = False
@@ -0,0 +1,90 @@
#!/usr/bin/env python
import sys
import logging
import logging.handlers
# The terminal has 8 colors with codes from 0 to 7
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
# These are the sequences need to get colored ouput
RESET_SEQ = "\033[0m"
COLOR_SEQ = "\033[1;%dm"
BOLD_SEQ = "\033[1m"
# The background is set with 40 plus the number of the color,
# and the foreground with 30
COLORS = {
'WARNING': COLOR_SEQ % (30 + YELLOW) + 'WARN ' + RESET_SEQ,
'INFO': COLOR_SEQ % (30 + WHITE) + 'INFO ' + RESET_SEQ,
'DEBUG': COLOR_SEQ % (30 + BLUE) + 'DEBUG' + RESET_SEQ,
'CRITICAL': COLOR_SEQ % (30 + YELLOW) + 'CRITI' + RESET_SEQ,
'ERROR': COLOR_SEQ % (30 + RED) + 'ERROR' + RESET_SEQ,
}
class ColoredFormatter(logging.Formatter):
def __init__(self, msg, use_color=True):
logging.Formatter.__init__(self, msg)
self.use_color = use_color
def format(self, record):
if self.use_color:
record.levelname = COLORS.get(record.levelname, record.levelname)
return logging.Formatter.format(self, record)
class LoggingConfiguration(object):
COLOR_FORMAT = "%(asctime)s" + \
" %(levelname)s %(message)s " + \
"(" + BOLD_SEQ + "%(filename)s" + RESET_SEQ + ":%(lineno)d)"
NO_COLOR_FORMAT = "%(asctime)s %(levelname)s " + \
"%(message)s " + \
"(%(filename)s:%(lineno)d)"
FILE_FORMAT = "%(asctime)s %(levelname)s " + \
"%(message)s "
@classmethod
def set(cls, log_level, log_filename, append=None, **kwargs):
""" Configure a rotating file logging
"""
logger = logging.getLogger()
logger.setLevel(log_level)
COLOR_FORMAT = cls.COLOR_FORMAT
NO_COLOR_FORMAT = cls.NO_COLOR_FORMAT
FILE_FORMAT = cls.FILE_FORMAT
if 'name' in kwargs:
COLOR_FORMAT = COLOR_FORMAT.replace('%(threadName)-22s',
'%-22s' % (kwargs['name']))
NO_COLOR_FORMAT = NO_COLOR_FORMAT.replace(
'%(threadName)-22s', '%-22s' % (kwargs['name']))
FILE_FORMAT = FILE_FORMAT.replace(
'%(threadName)-22s', '%s' % (kwargs['name']))
# Log to rotating file
try:
fh = logging.handlers.RotatingFileHandler(
log_filename,
mode='a+',
backupCount=3
)
fh.setFormatter(ColoredFormatter(FILE_FORMAT, False))
fh.setLevel(log_level)
logger.addHandler(fh)
if not append:
# Create a new log file on every new
fh.doRollover()
except IOError as e:
print('ignore to log to {}: {}'.format(log_filename, e))
# Log to sys.stderr using log level passed through command line
if log_level != logging.NOTSET:
log_handler = logging.StreamHandler(sys.stdout)
if sys.platform.find('linux') >= 0:
formatter = ColoredFormatter(COLOR_FORMAT)
else:
formatter = ColoredFormatter(NO_COLOR_FORMAT, False)
log_handler.setFormatter(formatter)
log_handler.setLevel(log_level)
logger.addHandler(log_handler)
@@ -0,0 +1,20 @@
backports.ssl-match-hostname==3.7.0.1
certifi==2019.9.11
chardet==3.0.4
Click==7.0
Flask==1.1.1
Flask-Login==0.4.1
gevent==1.4.0
gevent-websocket==0.10.1
greenlet==0.4.15
idna==2.8
itsdangerous==1.1.0
Jinja2==2.10.3
MarkupSafe==1.1.1
meld3==2.0.0
requests==2.22.0
six==1.12.0
supervisor==4.1.0
urllib3==1.25.6
websocket-client==0.47.0
Werkzeug==0.16.0
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env python
from __future__ import (
absolute_import, division, print_function, with_statement
)
import os
import time
import sys
import subprocess
from vnc.util import ignored
def main():
def run_with_reloader(main_func, extra_files=None, interval=3):
"""Run the given function in an independent python interpreter."""
def find_files(directory="./"):
for root, dirs, files in os.walk(directory):
for basename in files:
if basename.endswith('.py'):
filename = os.path.join(root, basename)
yield filename
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
try:
main_func()
except KeyboardInterrupt:
pass
return
proc = None
try:
while True:
log.info('Restarting with reloader {} {}'.format(
sys.executable,
' '.join(sys.argv))
)
args = [sys.executable] + sys.argv
new_environ = os.environ.copy()
new_environ['WERKZEUG_RUN_MAIN'] = 'true'
proc = subprocess.Popen(
args,
env=new_environ,
close_fds=True,
preexec_fn=os.setsid
)
mtimes = {}
restart = False
while not restart:
for filename in find_files():
try:
mtime = os.stat(filename).st_mtime
except OSError:
continue
old_time = mtimes.get(filename)
if old_time is None:
mtimes[filename] = mtime
continue
elif mtime > old_time:
log.info(
'Detected change in {}, reloading'.format(
filename
)
)
restart = True
proc.terminate()
break
time.sleep(interval)
except KeyboardInterrupt:
pass
finally:
with ignored(Exception):
proc.terminate()
def run_server():
import socket
from gevent.pywsgi import WSGIServer
from vnc.app import app
# websocket conflict: WebSocketHandler
if DEBUG:
# from werkzeug.debug import DebuggedApplication
app.debug = True
# app = DebuggedApplication(app, evalex=True)
try:
log.info('Listening on http://localhost:{}'.format(PORT))
http_server = WSGIServer(('localhost', PORT), app)
http_server.serve_forever()
# app.run(host='localhost', port=PORT)
except socket.error as e:
log.exception(e)
except KeyboardInterrupt:
pass
finally:
http_server.stop(timeout=10)
log.info('shutdown gracefully')
PORT = 6079
DEBUG = False
os.environ['CONFIG'] = 'config.Production'
entrypoint = run_server
if '--debug' in sys.argv:
DEBUG = True
os.environ['CONFIG'] = 'config.Development'
entrypoint = lambda: run_with_reloader(run_server)
# logging
import logging
from log.config import LoggingConfiguration
LoggingConfiguration.set(
logging.DEBUG if DEBUG else logging.INFO,
'/var/log/web.log'
)
logging.getLogger("werkzeug").setLevel(logging.WARNING)
log = logging.getLogger('novnc2')
entrypoint()
if __name__ == "__main__":
main()
+203
View File
@@ -0,0 +1,203 @@
from __future__ import (
absolute_import, division, print_function, with_statement
)
import re
import os
from flask import (
Flask,
request,
Response,
jsonify,
abort,
)
from gevent import subprocess as gsp, spawn, sleep
from geventwebsocket.exceptions import WebSocketError
from .response import httperror
from .util import ignored
from .state import state
from .log import log
# Flask app
app = Flask('novnc2')
app.config.from_object('config.Default')
app.config.from_object(os.environ.get('CONFIG') or 'config.Development')
@app.route('/api/state')
@httperror
def apistate():
state.wait(int(request.args.get('id', -1)), 30)
state.switch_video(request.args.get('video', 'false') == 'true')
mystate = state.to_dict()
return jsonify({
'code': 200,
'data': mystate,
})
@app.route('/api/health')
def apihealth():
if state.health:
return 'success'
abort(503, 'unhealthy')
@app.route('/api/reset')
def reset():
if 'w' in request.args and 'h' in request.args:
args = {
'w': int(request.args.get('w')),
'h': int(request.args.get('h')),
}
state.set_size(args['w'], args['h'])
state.apply_and_restart()
# check all running
for i in range(40):
if state.health:
break
sleep(1)
log.info('wait services is ready...')
else:
return jsonify({
'code': 500,
'errorMessage': 'service is not ready, please restart container'
})
return jsonify({'code': 200})
@app.route('/resize')
@httperror
def apiresize():
state.reset_size()
return '<html><head><script type = "text/javascript">var h=window.location.href;window.location.href=h.substring(0,h.length-6);</script></head></html>'
@app.route('/api/live.flv')
@httperror
def liveflv():
def generate():
xenvs = {
'DISPLAY': ':1',
}
bufsize = 1024 * 1
framerate = 20
# sound
sound_cmd_input = []
sound_cmd_parameters = []
zero_latency_make_sound_not_good = [
'-tune', 'zerolatency',
]
xenvs['X_WIDTH'] = state.w
xenvs['X_HEIGHT'] = state.h
xenvs['X_WIDTH'] -= state.w % 2
xenvs['X_HEIGHT'] -= state.h % 2
pixels_count = xenvs['X_WIDTH'] * xenvs['X_HEIGHT']
# factor (720p)
# 383: 2400k
# 300: 3000k
# 230: 4000k
factor = 265
maxbitrate_cmd = [
'-maxrate', str(int(pixels_count / factor)) + 'k',
'-bufsize', str(int(pixels_count / factor / 3)) + 'k'
]
# TODO move to global
# get default source
sound_cmd_input = [
'-f', 'alsa',
'-i', 'hw:2,1',
]
sound_cmd_parameters = [
'-ar', '44100',
'-c:a', 'mp3',
]
# flv.js report error if enabling hw acceleration
# hwaccel_dev = ['-vaapi_device', '/dev/dri/renderD128']
# hwaccel_if = ['-vf', 'format=nv12,hwupload']
# vcodec = 'h264_vaapi'
hwaccel_dev = []
hwaccel_if = []
vcodec = 'libx264'
# zero_latency_make_sound_not_good = []
# sound_cmd_parameters = []
# sound_cmd_input = []
cmd = ['/usr/local/ffmpeg/ffmpeg'] + sound_cmd_input + hwaccel_dev + [
'-video_size', '{X_WIDTH}x{X_HEIGHT}'.format(**xenvs),
'-framerate', '{}'.format(framerate),
'-f', 'x11grab', '-draw_mouse', '1',
'-i', '{DISPLAY}'.format(**xenvs),
] + hwaccel_if + [
'-r', '{}'.format(framerate),
'-g', '{}'.format(framerate),
'-flags:v', '+global_header',
'-vcodec', vcodec,
'-preset', 'ultrafast',
'-b_strategy', '0',
'-pix_fmt', 'yuv420p',
'-bsf:v', 'dump_extra=freq=e',
] + maxbitrate_cmd \
+ sound_cmd_parameters + zero_latency_make_sound_not_good + [
'-f', 'flv', 'pipe:1',
]
log.info('command: ' + ' '.join(cmd))
pobj = gsp.Popen(
cmd,
stdout=gsp.PIPE,
stderr=gsp.PIPE,
env={k: str(v) for k, v in xenvs.iteritems()},
)
def readerr(f):
reobj = re.compile(r'bitrate=(\S+)')
global av_bitrate
try:
while True:
buf = f.read(bufsize)
if len(buf) == 0:
break
patterns = reobj.findall(buf.decode('utf-8', 'ignore'))
if len(patterns) > 0:
av_bitrate = patterns[-1]
# log.info(str(buf))
except Exception as e:
log.exception(e)
preaderr = None
try:
preaderr = spawn(readerr, pobj.stderr)
try:
while True:
buf = pobj.stdout.read(bufsize)
if len(buf) == 0:
break
# ws.send(buf)
yield buf
except WebSocketError:
pass
except Exception as e:
log.exception(e)
finally:
with ignored(Exception):
pobj.kill()
preaderr.join()
except Exception as e:
log.exception(e)
finally:
log.info('exiting')
with ignored(Exception):
pobj.kill()
with ignored(Exception):
preaderr.kill()
log.info('exited')
return Response(generate(), mimetype='video/x-flv')
if __name__ == '__main__':
app.run(host=app.config['ADDRESS'], port=app.config['PORT'])
@@ -0,0 +1,2 @@
import logging
log = logging.getLogger('novnc2')
@@ -0,0 +1,40 @@
from __future__ import (
absolute_import, division, print_function, with_statement
)
from functools import wraps
import logging
from flask import jsonify
log = logging.getLogger()
class PermissionDenied(Exception):
pass
class BadRequest(Exception):
pass
def httperror(f):
@wraps(f)
def func(*args, **kwargs):
result = {
'code': 400,
'errorMessage': '',
}
try:
return f(*args, **kwargs)
except PermissionDenied as e:
result['code'] = 403
result['errorMessage'] = str(e)
except BadRequest as e:
result['code'] = 400
result['errorMessage'] = str(e)
except Exception as e:
logging.exception(e)
result['code'] = 500
result['errorMessage'] = str(e)
return jsonify(result)
return func
@@ -0,0 +1,133 @@
from __future__ import (
absolute_import, division, print_function, with_statement
)
from os import environ
from gevent.event import Event
from gevent import subprocess as gsp
from re import search as research
from .log import log
class State(object):
def __init__(self):
self._eid = 0
self._event = Event()
self._w = self._h = self._health = None
self.size_changed_count = 0
def wait(self, eid, timeout=5):
if eid < self._eid:
return
self._event.clear()
self._event.wait(timeout)
return self._eid
def notify(self):
self._eid += 1
self._event.set()
def _update_health(self):
health = True
output = gsp.check_output([
'supervisorctl', '-c', '/etc/supervisor/supervisord.conf',
'status'
])
for line in output.strip().split('\n'):
if not line.startswith('web') and line.find('RUNNING') < 0:
health = False
break
if self._health != health:
self._health = health
self.notify()
return self._health
def to_dict(self):
self._update_health()
state = {
'id': self._eid,
'config': {
'fixedResolution': 'RESOLUTION' in environ,
'sizeChangedCount': self.size_changed_count
}
}
self._update_size()
state.update({
'width': self.w,
'height': self.h,
})
return state
def set_size(self, w, h):
gsp.check_call((
'sed -i \'s#'
'^exec /usr/bin/Xvfb.*$'
'#'
'exec /usr/bin/Xvfb :1 -screen 0 {}x{}x16'
'#\' /usr/local/bin/xvfb.sh'
).format(w, h), shell=True)
self.size_changed_count += 1
def apply_and_restart(self):
gsp.check_call([
'supervisorctl', '-c', '/etc/supervisor/supervisord.conf',
'restart', 'x:'
])
self._w = self._h = self._health = None
self.notify()
def switch_video(self, onoff):
xenvs = {
'DISPLAY': ':1',
}
try:
cmd = 'nofb' if onoff else 'fb'
gsp.check_output(['x11vnc', '-remote', cmd], env=xenvs)
except gsp.CalledProcessError as e:
log.warn('failed to set x11vnc fb: ' + str(e))
def _update_size(self):
if self._w is not None and self._h is not None:
return
xenvs = {
'DISPLAY': ':1',
}
try:
output = gsp.check_output([
'x11vnc', '-query', 'dpy_x,dpy_y'
], env=xenvs).decode('utf-8')
mobj = research(r'dpy_x:(\d+).*dpy_y:(\d+)', output)
if mobj is not None:
w, h = int(mobj.group(1)), int(mobj.group(2))
changed = False
if self._w != w:
changed = True
self._w = w
if self._h != h:
changed = True
self._h = h
if changed:
self.notify()
except gsp.CalledProcessError as e:
log.warn('failed to get dispaly size: ' + str(e))
def reset_size(self):
self.size_changed_count = 0
@property
def w(self):
return self._w
@property
def h(self):
return self._h
@property
def health(self):
self._update_health()
return self._health
state = State()
@@ -0,0 +1,15 @@
from __future__ import (
absolute_import, division, print_function, with_statement
)
from contextlib import contextmanager
from gevent import GreenletExit
@contextmanager
def ignored(*exceptions):
try:
yield
except GreenletExit as e:
raise e
except exceptions:
pass
Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 KiB

@@ -0,0 +1,18 @@
[*]
wallpaper_mode=stretch
wallpaper_common=0
wallpapers_configured=4
wallpaper0=/usr/local/share/doro-lxde-wallpapers/bg1.jpg
wallpaper1=/usr/local/share/doro-lxde-wallpapers/bg2.jpg
wallpaper2=/usr/local/share/doro-lxde-wallpapers/bg3.jpg
wallpaper3=/usr/local/share/doro-lxde-wallpapers/bg4.jpg
desktop_bg=#000000
desktop_fg=#ffffff
desktop_shadow=#000000
desktop_font=Sans 12
show_wm_menu=0
sort=mtime;ascending;mingle;
show_documents=0
show_trash=0
show_mounts=0
@@ -0,0 +1,139 @@
[Desktop Entry]
Version=1.0
Name=Chromium Web Browser Sound
GenericName=Web Browser
GenericName[ar]=متصفح الشبكة
GenericName[ast]=Restolador web
GenericName[bg]=Уеб браузър
GenericName[bn]=ওয়েব ব্রাউজার
GenericName[bs]=Web preglednik
GenericName[ca]=Navegador web
GenericName[ca@valencia]=Navegador web
GenericName[cs]=WWW prohlížeč
GenericName[da]=Browser
GenericName[de]=Web-Browser
GenericName[el]=Περιηγητής ιστού
GenericName[en_AU]=Web Browser
GenericName[en_GB]=Web Browser
GenericName[eo]=Retfoliumilo
GenericName[es]=Navegador web
GenericName[et]=Veebibrauser
GenericName[eu]=Web-nabigatzailea
GenericName[fi]=WWW-selain
GenericName[fil]=Web Browser
GenericName[fr]=Navigateur Web
GenericName[gl]=Navegador web
GenericName[gu]=વેબ બ્રાઉઝર
GenericName[he]=דפדפן אינטרנט
GenericName[hi]=वेब ब्राउज़र
GenericName[hr]=Web preglednik
GenericName[hu]=Webböngésző
GenericName[hy]=Ոստայն զննարկիչ
GenericName[ia]=Navigator del Web
GenericName[id]=Peramban Web
GenericName[it]=Browser web
GenericName[ja]=ウェブ・ブラウザ
GenericName[ka]=ვებ ბრაუზერი
GenericName[kn]=ಜಾಲ ವೀಕ್ಷಕ
GenericName[ko]=웹 브라우저
GenericName[kw]=Peurel wias
GenericName[lt]=Žiniatinklio naršyklė
GenericName[lv]=Tīmekļa pārlūks
GenericName[ml]=വെബ് ബ്രൌസര്‍
GenericName[mr]=वेब ब्राऊजर
GenericName[ms]=Pelayar Web
GenericName[nb]=Nettleser
GenericName[nl]=Webbrowser
GenericName[or]=ଓ୍ବେବ ବ୍ରାଉଜର
GenericName[pl]=Przeglądarka WWW
GenericName[pt]=Navegador Web
GenericName[pt_BR]=Navegador web
GenericName[ro]=Navigator de Internet
GenericName[ru]=Веб-браузер
GenericName[sk]=WWW prehliadač
GenericName[sl]=Spletni brskalnik
GenericName[sr]=Интернет прегледник
GenericName[sv]=Webbläsare
GenericName[ta]=இணைய உலாவி
GenericName[te]=మహాతల అన్వేషి
GenericName[th]=เว็บเบราว์เซอร์
GenericName[tr]=Web Tarayıcı
GenericName[ug]=توركۆرگۈ
GenericName[uk]=Навігатор Тенет
GenericName[vi]=Bộ duyệt Web
GenericName[zh_CN]=网页浏览器
GenericName[zh_HK]=網頁瀏覽器
GenericName[zh_TW]=網頁瀏覽器
Comment=Access the Internet
Comment[ar]=الدخول إلى الإنترنت
Comment[ast]=Accesu a Internet
Comment[bg]=Достъп до интернет
Comment[bn]=ইন্টারনেটে প্রবেশ করুন
Comment[bs]=Pristup internetu
Comment[ca]=Accediu a Internet
Comment[ca@valencia]=Accediu a Internet
Comment[cs]=Přístup k internetu
Comment[da]=Få adgang til internettet
Comment[de]=Internetzugriff
Comment[el]=Πρόσβαση στο Διαδίκτυο
Comment[en_AU]=Access the Internet
Comment[en_GB]=Access the Internet
Comment[eo]=Akiri interreton
Comment[es]=Acceda a Internet
Comment[et]=Pääs Internetti
Comment[eu]=Sartu Internetera
Comment[fi]=Käytä internetiä
Comment[fil]=I-access ang Internet
Comment[fr]=Accéder à Internet
Comment[gl]=Acceda a Internet
Comment[gu]=ઇંટરનેટ ઍક્સેસ કરો
Comment[he]=גישה לאינטרנט
Comment[hi]=इंटरनेट तक पहुंच स्थापित करें
Comment[hr]=Pristupite Internetu
Comment[hu]=Az internet elérése
Comment[hy]=Մուտք համացանց
Comment[ia]=Accede a le Interrete
Comment[id]=Akses Internet
Comment[it]=Accesso a Internet
Comment[ja]=インターネットにアクセス
Comment[ka]=ინტერნეტში შესვლა
Comment[kn]=ಇಂಟರ್ನೆಟ್ ಅನ್ನು ಪ್ರವೇಶಿಸಿ
Comment[ko]=인터넷에 연결합니다
Comment[kw]=Hedhes an Kesrosweyth
Comment[lt]=Interneto prieiga
Comment[lv]=Piekļūt internetam
Comment[ml]=ഇന്റര്‍‌നെറ്റ് ആക്‌സസ് ചെയ്യുക
Comment[mr]=इंटरनेटमध्ये प्रवेश करा
Comment[ms]=Mengakses Internet
Comment[nb]=Bruk internett
Comment[nl]=Verbinding maken met internet
Comment[or]=ଇଣ୍ଟର୍ନେଟ୍ ପ୍ରବେଶ କରନ୍ତୁ
Comment[pl]=Skorzystaj z internetu
Comment[pt]=Aceder à Internet
Comment[pt_BR]=Acessar a internet
Comment[ro]=Accesați Internetul
Comment[ru]=Доступ в Интернет
Comment[sk]=Prístup do siete Internet
Comment[sl]=Dostop do interneta
Comment[sr]=Приступите Интернету
Comment[sv]=Surfa på Internet
Comment[ta]=இணையத்தை அணுகுதல்
Comment[te]=ఇంటర్నెట్‌ను ఆక్సెస్ చెయ్యండి
Comment[th]=เข้าถึงอินเทอร์เน็ต
Comment[tr]=İnternet'e erişin
Comment[ug]=ئىنتېرنېت زىيارىتى
Comment[uk]=Доступ до Інтернету
Comment[vi]=Truy cập Internet
Comment[zh_CN]=访问互联网
Comment[zh_HK]=連線到網際網路
Comment[zh_TW]=連線到網際網路
Exec=/usr/local/bin/chromium-browser-sound.sh %U
Terminal=false
X-MultipleArgs=false
Type=Application
Icon=chromium-browser
Categories=Network;WebBrowser;
MimeType=text/html;text/xml;application/xhtml_xml;x-scheme-handler/http;x-scheme-handler/https;
StartupNotify=true
Actions=NewWindow;Incognito;TempProfile;
X-AppInstall-Package=chromium-browser
+23
View File
@@ -0,0 +1,23 @@
[General]
leave_confirmation=true
[Environment]
GTK_CSD=0
GTK_OVERLAY_SCROLLING=0
[Mouse]
cursor_size=18
cursor_theme=whiteglass
acc_factor=20
acc_threshold=10
left_handed=false
[Keyboard]
delay=500
interval=30
beep=false
[Font]
antialias=true
hinting=true
dpi=96