flet-code-editor-dsl
A real Flutter code editor exposed as a Flet control, with a small Python DSL for defining syntax-highlighting rules and bracket/quote pair matching.
Table of Contents
- Overview
- Features
- Requirements
- Installation
- Quick Start
- API Reference
- How the Highlighting Engine Works
- Controlling the Editor at Runtime
- Complete Example
- Building Your App
- Project Structure
- Known Limitations
- Troubleshooting
- Contributing
- License
Overview
flet-code-editor-dsl is a Flet extension that embeds a multi-line code editor built on Flutter's TextField. Instead of shipping a fixed set of language grammars, it provides a compact DSL (domain-specific language) in Python that lets you describe exactly how text should be colored:
- Rules — regular expressions mapped to colors, with optional context conditions (
before,after,ignore_if). - Pairs — matching delimiters (brackets, quotes, tags) with distinct colors for matched and unmatched occurrences.
- Strict mode — any text not covered by a rule can be flagged as an error.
This makes it well suited for custom languages, configuration formats, templating syntaxes, and educational tools.
Features
- Native Flutter rendering through the Flet extension mechanism.
- Declarative highlighting rules defined entirely from Python.
- Context-aware matching using look-behind (
before), look-ahead (after) and exclusion (ignore_if) patterns. - Grouped rules: several patterns sharing a single color.
- Bracket and quote pair matching, including nesting and unmatched-delimiter detection.
- Strict mode that paints unrecognized text with an error color.
- Configurable font family, font size and background color.
on_changeevent for reading the current text.- Inherits standard layout properties from
ft.LayoutControl(width,height,expand,margin, etc.).
Requirements
| Component | Version |
|---|---|
| Python | >= 3.10 |
| Flet | >= 0.80 (the Flutter side depends on flet: ^0.80.0) |
| Dart SDK | >= 3.0.0 < 4.0.0 |
| Flutter | >= 3.10.0 |
| Git | Required for installing from GitHub |
Note: Because this package contains a Flutter extension, it only takes effect when your app is packaged with
flet build(or run with a Flet client that includes the extension). See Building Your App.
Installation
Option 1: Install directly from GitHub with pip
pip install "git+https://github.com/obgwew/flet-code-editor-dsl.git"
Or, using the explicit direct-reference form:
pip install "flet-code-editor-dsl @ git+https://github.com/obgwew/flet-code-editor-dsl.git"
Option 2: Declare it as a dependency in your Flet app
Add the package to the dependencies of your app's pyproject.toml:
[project]
name = "my-flet-app"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"flet",
"flet-code-editor-dsl @ git+https://github.com/obgwew/flet-code-editor-dsl.git",
]
Then install and run as usual:
pip install -e .
flet run
If you use a requirements.txt instead:
flet
git+https://github.com/obgwew/flet-code-editor-dsl.git
Pinning a version
Append @<ref> to the URL, where <ref> is a branch, tag, or commit hash:
# Branch
pip install "git+https://github.com/obgwew/flet-code-editor-dsl.git@main"
# Tag
pip install "git+https://github.com/obgwew/flet-code-editor-dsl.git@v0.1.0"
# Commit
pip install "git+https://github.com/obgwew/flet-code-editor-dsl.git@<commit-sha>"
Pinning to a tag or commit is strongly recommended for reproducible builds.
Option 3: Local editable install
git clone https://github.com/obgwew/flet-code-editor-dsl.git
cd flet-code-editor-dsl
pip install -e .
Verifying the installation
python -c "import flet_code_editor_dsl as m; print(m.__all__)"
Expected output:
['CodeEditor', 'rule', 'group', 'pair', 'rules_json', 'pairs_json']
Quick Start
import flet as ft
import flet_code_editor_dsl as ce
def main(page: ft.Page):
page.title = "Code Editor"
editor = ce.CodeEditor(
value="let x = (1 + 2)",
rules=ce.rules_json([
ce.group(["let", "if", "else"], color="#C586C0"), # keywords
ce.rule(r"\d+", color="#B5CEA8"), # numbers
]),
pairs=ce.pairs_json([
ce.pair("(", ")", color="#FFD700"),
]),
default_color="#D4D4D4",
background_color="#1E1E1E",
expand=True,
)
page.add(editor)
ft.run(main)
On Flet versions that still use the older entry point, replace
ft.run(main)withft.app(main).
API Reference
All public names are importable from the top-level package:
from flet_code_editor_dsl import CodeEditor, rule, group, pair, rules_json, pairs_json
CodeEditor
@ft.control("flet_code_editor_dsl")
class CodeEditor(ft.LayoutControl): ...
| Property | Type | Default | Description |
|---|---|---|---|
value |
str |
"" |
The text shown in the editor. Setting it from Python replaces the editor content. |
rules |
str (JSON) |
"[]" |
Highlighting rules. Build it with rules_json([...]). |
pairs |
str (JSON) |
"[]" |
Delimiter pairs. Build it with pairs_json([...]). |
default_color |
str |
"#000000" |
Color for text not matched by any rule (when strict is False). |
strict |
bool |
False |
If True, unmatched text is painted with error_color instead of default_color. |
error_color |
str |
"#FF0000" |
Color used for unrecognized text in strict mode. |
background_color |
ft.ColorValue | None |
None |
Editor background. When None, the Flutter side falls back to #202124. |
font_family |
str |
"monospace" |
Font family used by the editor. |
font_size |
float |
14.0 |
Font size in logical pixels. |
on_change |
ft.ControlEventHandler | None |
None |
Called whenever the text changes. The new text is available as e.data. |
In addition, all ft.LayoutControl properties (such as width, height, expand, margin, visible, opacity) are supported.
Tip: The default
default_coloris black (#000000) while the default background is dark. Always setdefault_colorexplicitly (for example"#D4D4D4") when using a dark theme.
Color format
Colors are strings in one of these forms:
| Format | Example | Meaning |
|---|---|---|
#RRGGBB |
#FF8800 |
Opaque color (alpha FF is added automatically). |
#AARRGGBB |
#80FF8800 |
Color with explicit alpha channel. |
Invalid values fall back to a default color instead of raising an error.
rule()
Creates a single highlighting rule.
rule(
form: str,
color: str,
before: Optional[str] = None,
after: Optional[str] = None,
ignore_if: Optional[str] = None,
) -> dict
| Parameter | Description |
|---|---|
form |
Regular expression that selects the text to color. |
color |
Color applied to every match. |
before |
Regex that must match immediately before the match (it is anchored to the end of the preceding text). If it does not match, the occurrence is skipped. |
after |
Regex that must match immediately after the match (anchored to the start of the following text). |
ignore_if |
Regex tested at the start of the match. If it matches, the occurrence is skipped. |
# Highlight identifiers that directly follow "def "
ce.rule(r"[A-Za-z_]\w*", color="#DCDCAA", before=r"def\s+")
# Highlight names followed by "(" as function calls
ce.rule(r"[A-Za-z_]\w*", color="#DCDCAA", after=r"\(")
# Highlight words, but skip the keyword "let"
ce.rule(r"[A-Za-z_]\w*", color="#9CDCFE", ignore_if=r"let\b")
group()
Creates a rule that applies one color to several patterns. The patterns are combined into a single alternation.
group(
forms: list[str],
color: str,
before: Optional[str] = None,
after: Optional[str] = None,
ignore_if: Optional[str] = None,
) -> dict
ce.group([r"\bif\b", r"\belse\b", r"\bwhile\b", r"\breturn\b"], color="#C586C0")
pair()
Defines a pair of delimiters that are matched against each other.
pair(
open: str,
close: str,
color: Optional[str] = None,
unmatched_color: str = "#FF0000",
) -> dict
| Parameter | Description |
|---|---|
open |
Opening delimiter (a literal string, not a regex). |
close |
Closing delimiter (a literal string). |
color |
Color for correctly matched delimiters. If None, matched delimiters keep the color assigned by rules. |
unmatched_color |
Color for delimiters that have no partner. Defaults to red. |
Behavior:
- When
open != close(e.g.(and)), delimiters are matched using a stack, so nesting is supported. An unmatched closer, or an opener left on the stack, is painted withunmatched_color. - When
open == close(e.g."and"), occurrences are counted in order. If the total count is odd, the last one is treated as unmatched.
ce.pair("(", ")", color="#FFD700")
ce.pair("[", "]", color="#DA70D6")
ce.pair("{", "}", color="#179FFF")
ce.pair('"', '"', color="#CE9178", unmatched_color="#FF5555")
rules_json() and pairs_json()
Serialize a list of rules or pairs into the compact JSON string expected by CodeEditor.rules and CodeEditor.pairs.
rules_json(rules: Iterable[dict]) -> str
pairs_json(pairs: Iterable[dict]) -> str
editor.rules = ce.rules_json([ce.rule(r"\d+", "#B5CEA8")])
editor.pairs = ce.pairs_json([ce.pair("(", ")", "#FFD700")])
How the Highlighting Engine Works
Understanding the evaluation order helps you write predictable rules.
- Rule pass — Rules are applied in the order they appear in the list. Each match paints its characters with the rule's color. Later rules override earlier ones where matches overlap.
- Context checks — For every match,
before,afterandignore_ifare evaluated. If a check fails, that match is ignored. - Fallback pass — Characters not covered by any rule receive
default_color, orerror_colorifstrict=True. - Pair pass — Pair matching runs last and overrides the colors of delimiter characters. This ensures unmatched delimiters are always visible.
Practical consequences
- Put general rules first (identifiers, words) and specific rules last (keywords, literals).
- Regular expressions use the Dart
RegExpengine (ECMAScript-style), not Python'sre. Most common syntax is identical, but avoid Python-only constructs such as(?P<name>...)— use(?<name>...)instead. - Patterns that fail to compile are silently skipped, so test new patterns incrementally.
- Use raw strings (
r"...") in Python to avoid double escaping.
Controlling the Editor at Runtime
Reading the text
def on_change(e: ft.ControlEvent):
print("Current text:", e.data)
editor = ce.CodeEditor(on_change=on_change)
Setting the text
editor.value = "new content"
editor.update()
Updating rules or pairs dynamically
editor.rules = ce.rules_json([
ce.rule(r"\bTODO\b", "#FFCC00"),
])
editor.update()
Switching strict mode
editor.strict = True
editor.error_color = "#FF5555"
editor.update()
Changing appearance
editor.font_size = 16
editor.font_family = "Courier New"
editor.background_color = "#0D1117"
editor.update()
Layout
ce.CodeEditor(width=600, height=400) # fixed size
ce.CodeEditor(expand=True) # fill available space
Complete Example
A live-validating editor for a tiny expression language. Keywords, numbers, strings and comments are colored, brackets are matched, and anything unrecognized is flagged as an error.
import flet as ft
import flet_code_editor_dsl as ce
SAMPLE = '''\
# Example program
let total = (price * 3) + 12
if (total > 100) {
print("expensive")
} else {
print("cheap")
}
'''
RULES = ce.rules_json([
# Identifiers (general rule first)
ce.rule(r"[A-Za-z_]\w*", "#9CDCFE"),
# Function calls: a name followed by "("
ce.rule(r"[A-Za-z_]\w*", "#DCDCAA", after=r"\("),
# Keywords
ce.group([r"\blet\b", r"\bif\b", r"\belse\b"], "#C586C0"),
# Numbers
ce.rule(r"\b\d+(?:\.\d+)?\b", "#B5CEA8"),
# Operators and whitespace (so strict mode does not flag them)
ce.group([r"[+\-*/=<>!]+", r"\s+", r"[(){}\[\],;]"], "#D4D4D4"),
# Strings
ce.rule(r'"[^"\n]*"', "#CE9178"),
# Comments (last, so they override everything on the line)
ce.rule(r"#.*", "#6A9955"),
])
PAIRS = ce.pairs_json([
ce.pair("(", ")", color="#FFD700"),
ce.pair("{", "}", color="#179FFF"),
ce.pair("[", "]", color="#DA70D6"),
])
def main(page: ft.Page):
page.title = "flet-code-editor-dsl demo"
page.theme_mode = ft.ThemeMode.DARK
status = ft.Text("Characters: %d" % len(SAMPLE))
def on_change(e: ft.ControlEvent):
status.value = "Characters: %d" % len(e.data or "")
status.update()
def toggle_strict(e: ft.ControlEvent):
editor.strict = e.control.value
editor.update()
editor = ce.CodeEditor(
value=SAMPLE,
rules=RULES,
pairs=PAIRS,
default_color="#D4D4D4",
strict=False,
error_color="#F44747",
background_color="#1E1E1E",
font_family="monospace",
font_size=15,
expand=True,
on_change=on_change,
)
page.add(
ft.Row([ft.Switch(label="Strict mode", on_change=toggle_strict), status]),
editor,
)
ft.run(main)
Building Your App
Flutter extensions are compiled into the Flet client, so you must build a custom client for your target platform. Make sure the package is listed in your app's pyproject.toml dependencies (see Installation), then run:
# Desktop
flet build windows
flet build macos
flet build linux
# Mobile
flet build apk
flet build ipa
# Web
flet build web
flet build detects the Flutter package bundled inside flutter/flet_code_editor_dsl/ and adds it to the generated Flutter project automatically. A working Flutter SDK is required; Flet will download one if it is missing.
Project Structure
flet-code-editor-dsl/
├── pyproject.toml
└── src/
├── flet_code_editor_dsl/
│ └── __init__.py # Python API: CodeEditor + DSL helpers
└── flutter/
├── __init__.py
└── flet_code_editor_dsl/
├── __init__.py
├── pubspec.yaml # Flutter package definition
└── lib/
├── flet_code_editor_dsl.dart
└── src/
├── extension.dart # Registers the control with Flet
└── code_editor.dart # Editor widget + highlighting engine
| File | Responsibility |
|---|---|
src/flet_code_editor_dsl/__init__.py |
Declares CodeEditor and the rule / group / pair helpers. |
.../lib/src/extension.dart |
Maps the control type flet_code_editor_dsl to the Flutter widget. |
.../lib/src/code_editor.dart |
Custom TextEditingController that builds colored TextSpans from rules and pairs. |
pyproject.toml |
Packaging metadata; includes pubspec.yaml and lib/**/* as package data. |
Known Limitations
- No language awareness in pairs. Delimiters inside strings or comments are still matched. Use
ignore_ifor rule ordering to reduce false positives, or avoid pairing characters that appear inside literals. - Highlighting is recomputed on each rebuild. Very large documents with many rules may reduce typing performance.
- Plain text editing only. Line numbers, code folding, auto-indent and autocompletion are not included.
- Dart regex semantics. Patterns must be valid for Dart's
RegExp; invalid patterns are ignored without an error message. - Client rebuild required. As with all Flet extensions, the control is not available in the stock Flet client.
- Minimum height. The editor shows at least 12 lines.
Troubleshooting
| Symptom | Likely cause and fix |
|---|---|
| The editor does not appear, or an "unknown control" error is shown | The app is running with a stock Flet client. Build with flet build, or ensure the extension is registered in your custom client. |
| Text is hard to see | default_color defaults to black while the background is dark. Set default_color (for example #D4D4D4) or change background_color. |
| A rule has no effect | Its regex may be invalid for Dart, or a later rule overrides it. Simplify the pattern and check rule order. |
| Everything is red | strict=True is set and some text is not covered by any rule. Add rules for whitespace, operators and punctuation, or disable strict mode. |
pip install from GitHub fails |
Confirm Git is installed, the URL is correct, and (for private repositories) that you are authenticated via SSH or a personal access token. |
| Changes made from Python are not visible | Call editor.update() after modifying a property. |
For private repositories, authenticate with a token:
pip install "git+https://<TOKEN>@github.com/obgwew/flet-code-editor-dsl.git"
Contributing
Contributions are welcome.
- Fork the repository.
- Create a feature branch:
git checkout -b feature/my-feature. - Commit your changes with clear messages.
- Push the branch and open a Pull Request describing the change and its motivation.
Please open an issue first for larger changes or new features so the design can be discussed.
License
This project is licensed under the Apache License, Version 2.0. See the LICENSE file for the full text.
Copyright 2026 obgwew
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
By contributing to this repository, you agree that your contributions will be licensed under the same Apache 2.0 license.
Metadata
Release files for flet-code-editor-dsl 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| flet_code_editor_dsl-0.1.0.tar.gz | 19.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| flet_code_editor_dsl-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 33.6 kB
Release files / flet_code_editor_dsl-0.1.0.tar.gz
| Download URL | flet_code_editor_dsl-0.1.0.tar.gz |
|---|---|
| Size | 19.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
308b133cc6bdd4ec6e9ba1b2a14d99171aae4419ebaf3591c8720b8dea7ddd08
|
|
BLAKE2b-256 checksum How to use checksums |
dbc0fbc2de119251c3d71550c5d9ad5d7712ef687cef75851277055c8754ae78
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|
Release files / flet_code_editor_dsl-0.1.0-py3-none-any.whl
| Download URL | flet_code_editor_dsl-0.1.0-py3-none-any.whl |
|---|---|
| Size | 13.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9fc063c19814c58d2b6d26e2a9f8885f9cb29192e554c27a5049f2e6583bba53
|
|
BLAKE2b-256 checksum How to use checksums |
c5af3ad164aff4dc0296f2bec53082245269ac18a607a033ff6eab7e8a9ff64f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|