flamelex is currently considered alpha
- not stable or usable by the
general public.
During the alpha phase of development (~Dec 2019 - ??), the branch name
changed several times, but it was actually just a continuous series of
commits from a single developer (JediLuke). This branch has been kept for
archaelogical reasons - once v0.2.7-alfonz
has been officially released,
the default branch will revert to trunk
, and development will move over
to the jediluke/develop
branch.
A combination text-editor & memex written in Elixir.
Flamelex is a self-contained Elixir app, build upon the Elixir GUI library
Scenic
. The main inspiration is emacs, especially the idea of having a
REPL that can be personalized. The text-editing experience is a ViM derivation,
so as one programmer has summarized it, "Flamelex is a Spacemacs clone in Elixir,
with built-in TiddlyWiki"
The GUI in Flamelex is built with Scenic. Scenic requires gfx drivers. The most up to date information on how to install Scenic for your platform can be found in the Scenic documentation
As of June 2022, for MacOS the easiest way is to use HomeBrew:
brew update
brew install glfw3 glew pkg-config
Simply navigate to a directory you would like to put a new git project.
git clone https://github.com/JediLuke/flamelex.git
cd flamelex
mix deps.get
iex -S mix run
Right now, because of the way Scenic works, we have to re-draw the gui
if we want to resize the window. The way we do this, is to change the
value of the GUI dimensions in the Flamelex.GUI.ScenicInitialize
(or,
the scenic_initialize.ex
file, same thing)
Fine the default_viewport_config
, declared at the top, and update the
size
key to a tuple. This is the number of pixels (citation needed) in
the new window. Restart Flamelex and voila, it should be the new size.
Note that right now, all objects in the GUI are hard-coded in size, so adjusting the size of the window may make things render stragely. In the future, we want to look to incorporating the Layout-o-Matic! library to get flexible sizes/layouts.
From the repository, simply start the program in dev
mode, the same way
you would start basically any Elixir program using Mix:
iex -S mix run
This gives you an IEx session, and should have displayed the default Flamelex window showing a "transmutation circle" and a version number: #TODO
We are going to start out by explaining the way to drive Flamelex, via
it's Elixir API - if you started Flamelex according to the instructions,
it is now running locally in a dev
Mix environment. This is how flamelex
is intended to operate - with an open IEx shell, where a user can enter
commands, and an open GUI, to give the user feedback and accept direct input.
Yes, you can interact with the flamelex GUI directly (the default editing
experience is similar to Vim/Emacs); but to get a feel for how everything
really works, let's start by using the Flamelex user-API modules in a
running IEx console session.
#TODO would be cool to have some kind of test function that just prints
the current version back to the user, so they can verify the install worked & all is OK up to this point
Go back to the IEx terminal you used to start flamelex, and type:
Buffer.open!("README.md")
You should see a new Buffer open - if you are coming from a Vim or Emacs background, Buffers are exactly what you expect them to be - a window into a data-stream, usually a text-file, which lets you inspect and modify the contents of that data stream (or just, Buffer == file, for my fellow simpletons).
##NOTE: Steps to add a new piece of functionality:
# 1) Create a new API function, in an API module
# 2) Create a reducer function, in a Reducer module <-- You are here.
# 3) Update related components to handle potential new states (just changing between known states should work already, assuming your components know how to render the new state)
At this point in the tutorial, we have successfully opened a text file (which is the same thing as saying, we have opened a text buffer), which happens to be the README.md file for this project. You can see a flashing cursor in the top left.
Let's say you want to move the cursor. If you know ViM, your instinct might be to reach for HJKL - and you are correct! But, for a short time, forget that - we have just a few more functions to run in the IEx console, to drive home the point that in flamelex, everything is a function call
Buffer.active_buffer()
#TODO show correct ouput here
At this point in the tutorial, we have already opened a buffer (the README
file for flamelex). So calling Buffer.active_buffer/0
returned a reference
to that buffer, because, that is the active buffer - being the active
buffer just means that it is the one we are interacting with at the moment,
certain commands are (by default) applied to whichever buffer is currently
designated as the active buffer.
Buffer.list()
Should show a list of buffers, exactly 1. You can open any other file using the following function:
#TODO WARNING - DONT DO THIS... ITS EXPERIMENTAL... IT MIGHT CRASH !!!
Buffer.open("/your/file/here.txt")
The buffer you just opened will now become the active buffer
. You can
assign this Buffer reference to a variable, as you can any other piece of
primitive data in Elixir:
b = Buffer.active_buffer()
Now to modify the buffer, use another function in the Flamelex.API.Buffer
module, modify/x
b = Buffer.active_buffer()
Buffer.modify(b, {:insert, "“The future depends on what you do today.”", 3})
This will insert some text (in this case, a quote), 3 characters (because that is the number we passed in as a parameter) of text from the beginning of the buffer. There are other, usually more convenient ways to tell flamelex where you want to modify a text buffer, e.g.
text_quote = Memex.random_quote().text
Buffer.active_buffer()
|> Buffer.modify({:insert, text_quote, {:cursor, 1}})
This will insert the text at the position of the first cursor. So if you want to move the cursor around:
Buffer.active_buffer()
|> Buffer.move_cursor(1, {:down, 2, :lines})
|> Buffer.move_cursor(1, {:right, 5, :columns})
|> Buffer.modify({:insert, Memex.My.first_name(), {:cursor, 1}})
If you are wondering what that Memex.My
stuff is all about... don't
worry, that's just a sneak peak. For now, just know that this function
returns a string of text, which should have just been inserted at the
position of cursor 1 in the active buffer.
Flamelex was implemented to be an API-driven application, right from the
initial commit. Every action you can take in flamelex is also possible
via the IEx console. When you type the letter "e" in :insert mode, that
is simply mapped to the function Buffer.modify(:insert, "e")
, and if you
were of a particular mood that you didn't want to type your text "directly
in" to the GUI (the so called WYSIWYG experience), but instead wanted to
do all your editing by calling the specific functions in the IEx console,
that would be totally possible - and hey, it's your life, I say do what
makes you happy! Let me know how it goes.
In the next section, we shall show the user some commands they can initiate via keystroke, when the software is in a specific input mode. However it is important to remember that behind the scenes, everything is just a combination of function calls, sending messages to stateful processes. How that mapping is defined, is covered next up.
# 3) Update related components to handle potential new states (just changing between known states should work already, assuming your components know how to render the new state)
Starting from the bottom left, you will see a box - this box tells you
what mode you are in - at this point in the tutorial, you should be
in normal mode. If you know the commands for ViM in normal mode, you
should feel right at home - press i
to enter :insert
mode, and away you
go.
If you aren't familiar with ViM... this, sorry, but this probably isn't your text editor at this point in time.
Here I take another moment to yet again explain that all inputs in
Flamelex are simply mappings. For the ViM commands, the way it works is
the input is collected and forwarded to a specific process ViMServer
which knows the input languag of ViM, remembers the last few keystrokes,
and translates that input language into calls using the Flamelex API -
but that's all it's doing, calling the same functions you called before
in the IEx console - you could create your entire own input mapping (or
go rip off kakoune or emacs or whoever you want), and without too much
effort, you could get it working in flamelex because the functionality
is de-coupled from the UI
To save the file, you can use the leader binding <space>s
or go back
to the IEx console and type Buffer.save()
The KommandBuffer is the flamelex version of M-x
(execute-extended-command)
in emacs. It brings the terminal directly into your editing experience.
The iex console is quite powerful, capable of storing variables and running basically any Elixir code. In many ways, flamelex is just a GUI wrapper around the iex shell, with some libraries around editing text thrown in. Flamelex was, from the ground up, intended to work like emacs in the sense that it is an interactive lisp shell, with a runtime of variables (though in our case, we like to back it up to disk) and functions, including functions which can edit text files.
historical note: The day I thought I had become an emacs convert for life,
was the day I discovered M-x
or execute-extended-command
. This command
in emacs brings up a lisp repl, right over your text files! I was a heavy
user of this feature, and I wrote many personal shortcut functions, which
were naturally all accessible via the M-x
shell. I liked this command
so much, that I re-mapped it to k, which is IMHO the most
ergonomic and efficient leader keymapping - it earned that spot, because
I used it so often.
THe first way to activate the KommandBuffer is, of course, by calling the appropriate function in iex. In this case we call:
KommandBuffer.show()
You may notice, that an input has appeared at the bottom of the screen:
#TODO show screenshot
Here, you can type in any Elixir command you like - it's not really any different from typing it into iex.
#TODO show example of, typing a function into iex, and typing one into KommandBuffer
Now, we don't really want to have to go use iex each time we want to use the KommandBuffer - that would kind of defeat the point. Instead, we can map this function call to some keypresses, so we can activate the KommandBuffer with some simple keystrokes.
When I implemented this feature in flamelex, I immediately mapped it to
k again, which is how it ended up with the nomenclature of
KommandBuffer
This mapping is completely arbitrary! As demonstrated earlier, you can just as easily open the KommandBuffer by calling the function in iex, as by pressing this, completely arbitrary, combination of keypresses. This is just the default ones that I like to use, because I use this feature a lot, and k is arguably the most ergonomic double-keystroke on the keyboard.
See the section: Handling user input
for a more detailed understanding
of how keymappings are achieved.
#TODO example:
The MenuBar is currently not functional, but the idea in the future is to link buttons in the MenuBar directly to modules/functions inside flamelex, so clicking one will just call that function, probably in it's own MenuBar supervision tree.
The way it works is this - users should only need to use the API to achieve what they want to do. If this isn't the case, then adding this functionality is not difficult, but it needs to be added in a way that's consistent with how flamelex works.
The API can call directly into the underlying sub-tree for information
requests, e.g. Buffer.list calls BufferManager, but to trigger actions,
it should only call Flamelex.Fluxus.fire_action/2
Then, these actions will go through Fluxus and eventually propagate through to the reducer. Then, in the reducers, that's where you can call things like Buffer.move_cursor, etc.
The unguarded nature of Elixir modules is both a strength and a weakness, and overall I prefer to be given the freedom to build amazing things with some gotchas, rather than be forced to jump through unnecessary hoops that just get in the way once I know what I'm doing - but this is a gotcha for adding code to Flamelex, you must go through the designated flows. If you start calling things like Buffer.move_cursor(2), it will probably work, but your whole state tree might get out of whack...
When developing or changing the functionality of the API, remember to respect
the rest of Flamelex as a seperate system, so we can't just reach into
the internals (even though Elixir would let us do that), because that's
going to start screwing things up! e.g. to implement Buffer.open
, we
must never call up BufferManager and directly request the Buffer be opened -
this breaks a whole chain of checks & event-triggers, starting way back
up at FluxusRadix. We don't just directly affect Flamelex, we instead
use the mechanism of firing actions, which correctly processes the input
and propagates it through the internal messaging infrastructure of Flamelex.
There are several famous & intelligent engineers who are strongly opposed to the concept of modes - and I respect their opinion; it is not without merit. Modes are complicated and unintuitive to the user. They require training and practice to use effectively - when that is achieved though, it has been my experience in using vim at least, that a very satisfying, intuitive (or perhaps just, performed in the "back of my mind", not the same part that likes to think about what I'm programming), efficient & economical form of Human-Computer interaction. So yeah, I think if people want to maximize their bandwidth when interacting with computers, I think it's worth learning a modal editor.
In a future life, when we're all Start Fleet officers, we can continue the discussion, about whether or not modes would be a good/bad idea in LCARS.
In Flamelex, I treat modes as a strictly user-input side concern. The
mode changes what happens when you press the buttons. It changes how
some things are rendered. However, nothing "internal" to Flamelex ever
changes. There is no concept of a mode in the Buffer
API, for example.
Changing the mode, doesn't affect the result of any of those underlying
functions, there is no internal state in that part of the application
which understands modes.
Right now, modes are global. Insert mode will put the active Buffer, into insert mode. It might be prudent at some point to scope modes to active (or not?) Buffers/Windows.
As a flamelex user, you shouldn't have to look "under the hood". You can, at any point in time, do so - but hopefully, unless you're merely interested, you won't ever have to.
All actions that a user can take are defined in the API modules, which are stored in the API directory. If a user wants to do something, and no combination of API functions is capable of making it happen, then there is no other way of doing it safely - the API modules must be updated. But they must be done so in a safe way, we never want to reach diretly into the internals of Flamelex, because we might screw things up!
What is a Memex? see: https://en.wikipedia.org/wiki/Memex
Think of the Memex as your personal wikipedia. It's a place to store all your knowledge and data, in a way that's retreivable and programmable (in Elixir no less!). In the Memex, you can store:
- Your favourite Elixir snippets
- Your wife's birthday
- Your latest beyond-brisket recipe
- Financial records
- kanban boards
- ...
- anything...
#TODO Example
iex> Memex.My.current_timezone() "Texas" #TODO iex> Memex.random_quote().text "Well done is better than well said."
The Memex is heavily inspired by Tiddlywiki.
French: épistémè) is a philosophical term that refers to a principled
system of understanding; scientific knowledge. The term comes from the
Ancient-Greek verb epístamai (ἐπῐ́στᾰμαι), meaning 'to know, to understand,
to be acquainted with'.
Because "knowledge base" looks rather ugly, I plucked this word episteme
out of ancient greek and used it to mean "any and all, pieces of codified
knowledge, stored in the Memex". So one's epistime is the grand sum of
all ones knowledge.
In Flamelex, the general_episteme
is all the "general" knowledge, common
to all users, which is accepted as general fact. It is, basically, an attempt
to clone all of Wikipedia in Elixir - imagine being able to access all of
Wikipedia, from the command line!! We are getting there, module by module,
func-def by func-def...
You need to add it to mix.exs, then add the memelex config in your config.ex
When you first open Flamelex, it will load the Memex with a sample
environment -
this is to give the user an introduction to the power of the Memex, via
example.
Memex.current_env()
Flamelex.Memex.Env.SampleEnv
Memex.My.timezone()
"Etc/UTC"
Memex.My.todos()
["mow the lawn", "call grandma"]
As you can see, the things in one's personal Memex are usually only of
interest to oneself, and if you're putting your flamelex codebase into
version control (which I, as the currently sole creator & user, obviously
do) you might want to keep some of this Memex knowledge secret, and not
checked in to Github. Also, to offer a multi-user perspective for users
of Flamelex, we use the Memex.My
interface throughout Flamelex - via
this interface, we reach into your own personal Memex and extract some
information, e.g. when opening up a new Journal entry, we look into the
Memex.My.current_timezone()
to detect the date & time of the user.
To load your own custom Memex, create a new directory in the lib/memex/environments
directory. Give the directory the same "name" you want to give your Memex
environment, e.g. my online tag (slack/github/etc) is "JediLuke", so that's
what I called my Memex environment. So my new file will be located at
lib/memex/environments/jediluke
.
In that directory, create a file, jediluke.ex
(or whatever your environment
is going to be called). This is going to be the highest-level Elixir module
in your environment, so that's the one where we are going to use the
Memex Environment behaviour.
defmodule Flamelex.Memex.Env.SampleEnv do
@moduledoc """
A sample Memex environment.
"""
use Flamelex.Memex.EnvironmentBehaviour
def timezone, do: "Etc/UTC"
def todo_list, do: [
"mow the lawn",
"read a philosophy book",
"call grandma"
]
def reminders, do: []
def journal, do: raise "Not implemented!"
end
For a more thorough example, please see: lib/memex/environments/sample/sample_env.ex
By calling use Flamelex.Memex.EnvironmentBehaviour
, you will inherit
a bunch of functions, and be forced to implement a bunch of callbacks that
every Memex environment should/must implement.
We also need to update the config variable which defines your Memex - to
do this, go to the Flamelex.API.Memex
and change the hard-coded value. #TODO
#TODO it looks like Tiddlywiki
#TODO
Flamelex has the built-in concept of agents
How to set a reminder using the Remidner agent...
Flamelex was born out of my frustration trying to create the "perfect" emacs/vim setup. I am a heavy modifier of these programs, but eventually hard to use APIs and bugs in the software (I consider emacs' inability to support multi-threading a bug in 2020) prompted me to "flip the desk" and start from scratch. I chose Elixir because it is a language I know and love, and because I think the immutable, functional style of Elixir code, as well as the fact it runs on the BEAM VM, will make Franklin a very stable editor.
Some of my main goals are:
- Easy for beginners. Comes with tutorial, full help, and good UX (alwa- ys give the user feedback!)
- Self-documenting
- Contains a personal memex, modelled on tiddlywiki
- REPL driven for absolute programmability
- modal-editing, but with inputs completely de-coupled from functionality
flamelex used the flux
architecture - we hold a store of state, and
use reducer functions, combined with actions/input (and they are capable
of generating side-effects, such as firing of other actions), to generate
the updated state - which is then rendered.
The starting point for firing any action
to the inner-workings of
flamelex is to look at the Flamelex.Fluxus
module. This module provides
the interface to firing these actions - when an action is fired, a message
is propagated through an entire tree of processes, which effectively
hold the state of the entire application between them.
User input gets picked up by the underlying Scenic drivers, and Scenic
then sends that input as a message to the process which is rendering
the root scene - see Flamelex.GUI.RootScene
.
Inside Flamelex.GUI.RootScene
there is a function, handle_input/3
-
this is where user-input is presented to us by Scenic. That's just how
Scenic works, keypresses show up here first. But we don't want to hold
our application state inside Scenic really, since we want to keep drawing
the GUI decoupled from the actual business-logic of editing text. So we
just immediately forward this user input to the Fluxus part of the app,
by calling Flamelex.Fluxus.handle_user_input(input)
when a user presses a key...
-> Flamelex.GUI.RootScene.handle_input/3
(root_scene.ex)
-> Flamelex.Fluxus.handle_user_input/1
(fluxus.ex)
-> Flamelex.FluxusRadix
receives {:user_input, ii}
via Genserver.cast
(fluxus_radix.ex)
-> calls Flamelex.Fluxus.Radix.UserInputHandler.handle/2
(user_input_handler.ex)
-> spins up a new Task
process, executing
lookup_action_for_input_async/2
under InputHandler.TaskSupervisor
-> that function will look in the key-mapping module, e.g.
Flamelex.KeyMappings.Vim
if this lookup fails/crashes,
no problem really. If a lookup is successful, then maybe
actions get fired, functions get called... whatever.
#DEVELOPING a new component Step 1 - figure out where you want to mount the component. #TODO this should be a layer I guess...
- it needs to get mounted in the GIU somewhere
warning - you are on the branch franklin_dev
This software's first working name was Franklin
, after the American
philosopher, inventor, and I suspect alchemist, Benjamin Franklin.
I was learning a lot about American history at the time, and when looking
for a good quote to initialize the branch, came across the apocryphal
quip that graces the git-log of the first commit:
“For every minute spent organizing, an hour is earned.” - Benjamin Franklin JediLuke on 12/28/2019, 11:49:22 PM
At some point I began throwing the name out there to some other programmers,
and got feedback that frankin
was too generic, there were other packages
in other languages that already used it, etc... I had already gotten very
into the alchemist theme by this point, so decided to change the name to
flamelex
after the famous alchemist, Nicholas Flamel.
The first flamelex release was beginning to become finalized
around the start of 2021. Up until this point, all work was just one series of
commits by me, JediLuke. I decided to keep this series of commits as the
branch franklin_dev
, as a tip-o'-the-hat to Franklin, the original seed
that grew into Flamelex. Any code archaelogists out there?? here's a dig!
I went back & forth a lot over various design - before I totally understood gproc, I wasn't able to structure the GUI components in a heirarchical manner which made sense. The heirarchical tuples were (not the, just one) solution to this (now we use PubSub).
I also suffered from a lot of scope creep - I went from doing basic editing, to developing a new CLI GUI, to developing a TiddlyWIki, to developing software agents that are always running to help you. This version tries to show a MVP for all these features, but is just enough to "get it out the door" and show the community what I've done so far.`
- Ability to read documents & maintain my own notes on such documents
- Ability to do source-control inside the editor
- Integrated with Elixir compiler
- add MenuBar which is linked to calling functions & ability to read & search HexDocs
NEXT is MenuBar
- add ripgrep for fuzzy searching
- git tree
- hexdocs
- LLM agent coding assistance
- autocorrect / Elixir LSP / connect to runtime for real-time compilation