30 Apr 2024

feedPlanet Lisp

Joe Marshall: Statements and Expressions

In some languages, like Lisp and OCaml, every language construct is an expression that returns a value. Other languages, like Java or Python, have two kinds of language constructs: expressions, which combine compositionally and which have return values, and statements, which combine sequentially and which have no return values and thus must operate by side effect. Having statements in your language needlessly makes things more complicated, but language designers seem to want to go much further and add complexity that just seems capricious.

You cannot usually use a statement in a context where an expression is expected because there is no return value. You can use an expression where a statement is expected by simply discarding the return value. This means there are two kinds of contexts. A sane designer would provide a way to switch between statement and expression contexts, but language designers typically omit these.

Language constructs, such as binding or iteration, must be provided as statements or expressions or both. Language designers seem to randomly decide which constructs are statements and which are expressions based on whims and ease of compiler implementation. If you need to use a construct, but aren't in the right kind of context, you need to switch contexts, but without a way to do this, you may have to rewrite code. For example, if you have a subexpression that could raise an exception that you want to handle, you'll have to rewrite the containing expression as a series of statements.

A sane language designer would use the same syntax for the same construct in both expression and statement form, but typically the construct will have a very different syntax. Consider sequential statements in C. They are terminated by semicolons. But sequential expressions are separated by commas. Conditional statements use if/else, but conditional expressions use ?:. When refactoring, you cannot simply move code between contexts, you have to change the syntax as well.

Writing a function adds a new expression to the language. But function bodies aren't expressions, they are statements. This automatically destroys referential transparency because you cannot substitute the function body (statements) at the call site (expression context).

try/catch is a nightmare. Usually you only get try/catch as a statement, not an expression, so you cannot use exception handling in a subexpression. You cannot get a value out of a try/catch without a side effect. Throw, on the other hand, throws a value, so it could throw to an expression context, except that catch is a statement.

Having two kinds of language constructs and two different contexts in which you can use some of them but not others, and different syntaxes depending on the context just makes it that much more difficult to write programs. You have to keep track of which context is current and which syntax to use and constantly switch back and forth as you write a program. It would be easy for a compiler to introduce the necessary temporaries and rewrite the control flow so that you could use all language constructs in either context, but language designers don't bother and leave it up to the programmer to do it manually.

And all this mess can be avoided by simply making everything be an expression that returns a value.

30 Apr 2024 4:18pm GMT

25 Apr 2024

feedPlanet Lisp

Joe Marshall: State Machines

One of the things you do when writing a game is to write little state machines for objects that have non-trivial behaviors. A game loop runs frequently (dozens to hundreds of times a second) and iterates over all the state machines and advances each of them by one state. The state machines will appear to run in parallel with each other. However, there is no guarantee of what order the state machines are advanced, so care must be taken if a machine reads or modifies another machine's state.

CLOS provides a particularly elegant way to code up a state machine. The generic function step! takes a state machine and its current state as arguments. We represent the state as a keyword. An eql specialized method for each state is written.

(defclass my-state-machine ()
  ((state :initarg :initial-state :accessor state)))

(defgeneric step! (state-machine state))

(defmethod step! ((machine my-state-machine) (state (eql :idle)))  
  (when (key-pressed?)
    (setf (state machine) :keydown)))

(defmethod step! ((machine my-state-machine) (state (eql :keydown)))
  (unless (key-pressed?)
    (setf (state machine) :idle)))

The state variables of the state machine would be held in other slots in the CLOS instance.

One advantage we find here is that we can write an :after method on (setf state) that is eql specialized on the new state. For instance, in a game the :after method could start a new animation for an object.

(defmethod (setf state) :after ((new-state (eql :idle)) (machine my-state-machine))
  (begin-idle-animation! my-state-machine))

Now the code that does the state transition no longer has to worry about managing the animations as well. They'll be taken care of when we assign the new state.

Because we're using CLOS dispatch, the state can be a class instance instead of a keyword. This allows us to create parameterized states. For example, we could have a delay-until state that contained a timestamp. The step! method would compare the current time to the timestamp and go to the next state only if the time has expired.

(defclass delay-until ()
  ((timestamp :initarg :timestamp :reader timestamp)))

(defmethod step! ((machine my-state-machine) (state delay-until))
  (when (> (get-universal-time) (timestamp state))
    (setf (state machine) :active)))

Variations

Each step! method will typically have some sort of conditional followed by an assignment of the state slot. Rather that having our state methods work by side effect, we could make them purely functional by having them return the next state of the machine. The game loop would perform the assignment:

(defun game-loop (game)
  (loop
    (dolist (machine (all-state-machines game))
      (setf (state machine) (step machine (state machine))))))

(defmethod step ((machine my-state-machine) (state (eql :idle)))  
  (if (key-pressed?)
      :keydown
      :idle))

I suppose you could have state machines that inherit from other state machines and override some of the state transition methods from the superclass, but I would avoid writing such CLOS spaghetti. For any object you'll usually want exactly one state transition method per state. With one state transition method per state, we could dispense with the keyword and use the state transition function itself to represent the state.

(defun game-loop (game)
  (loop
    (dolist (machine (all-state-machines game))
      (setf (state machine) (funcall (state machine) machine)))))

(defun my-machine/state-idle (machine)
  (if (key-pressed?)
      (progn
         (incf (kestroke-count machine))
         #'my-machine/state-keydown)
      #'my-machine/state-idle))

(defun my-machine/state-keydown (machine)
  (if (key-pressed?)
      #'my-machine/state-keydown
      #'my-machine/state-idle))

The disadvantage of this doing it this way is that states are no longer keywords. They don't print nicely or compare easily. An advantage of doing it this way is that we no longer have to do a CLOS generic function dispatch on each state transition. We directly call the state transition function.

The game-loop function can be seen as a multiplexed trampoline. It sits in a loop and calls what was returned from last time around the loop. The state transition function, by returning the next state transition function, is instructing the trampoline to make the call. Essentially, each state transition function is tail calling the next state via this trampoline.

State machines without side effects

The state transition function can be a pure function, but we can remove the side effect in game-loop as well.

We keep parallel lists of machines and their states (represented as state transition functions).

(defun game-loop (machines states)
  (game-loop machines (map 'list #'funcall states machines)))

Now we have state machines and a driver loop that are pure functional.

25 Apr 2024 3:51pm GMT

19 Apr 2024

feedPlanet Lisp

Joe Marshall: Plaformer Game Tutorial

I was suprised by the interest in the code I wrote for learning the platformer game. It wasn't the best Lisp code. I just uploaded what I had.

But enough people were interested that I decided to give it a once over. At https://github.com/jrm-code-project/PlatformerTutorial I have a rewrite where each chapter of the tutorial has been broken off into a separate git branch. The code is much cleaner and several kludges and idioticies were removed (and I hope none added).

19 Apr 2024 9:01pm GMT

14 Apr 2024

feedPlanet Lisp

Paolo Amoroso: Testing the Practical Common Lisp code on Medley

When the Medley Interlisp Project began reviving the system around 2020, its Common Lisp implementation was in the state it had when commercial development petered out in the 1990s, mostly prior to the ANSI standard.

Back then Medley Common Lisp mostly supported CLtL1 plus CLOS and the condition system. Some patches submitted several years later to bring the language closer to CLtL2 needed review and integration.

Aside from these general areas there was no detailed information on what Medley missed or differed from ANSI Common Lisp.

In late 2021 Larry Masinter proposed to evaluate the ANSI compatibility of Medley Common Lisp by running the code of popular Common Lisp books and documenting any divergences. In March of 2024 I set to work to test the code of the book Practical Common Lisp by Peter Seibel.

I went over the book chapter by chapter and completed a first pass, documenting the effort in a GitHub issue and a series of discussion posts. In addition I updated a running list of divergences from ANSI Common Lisp.

Methodology

Part of the code of the book is contained in the examples in the text and the rest in the downloadable source files, which constitute some more substantial projects.

To test the code on Medley I evaluated the definitions and expressions at a Xerox Common Lisp Exec, noting any errors or differences from the expected outcomes. When relevant source files were available I loaded them prior to evaluating the test expressions so that any required definitions and dependencies were present. ASDF hasn't been ported to Medley, so I loaded the files manually.

Adapting the code

Before running the code I had to apply a number of changes. I filled in any missing function and class definitions the book leaves out as incidental to the exposition. This also involved adding appropriate function calls and object instantiations to exercise the definitions or produce the expected output.

The source files of the book needed adaptation too due to the way Medley handles pure Common Lisp files.

Skipped code

The text and source files contain also code I couldn't run because some features are known to be missing from Medley, or key dependencies can't be fulfilled. For example, a few chapters rely on the AllegroServe HTTP server which doesn't run on Medley. Although Medley does have a XNS network stack, providing the TCP/IP network functions AllegroServe assumes would be a major project.

Some chapters depend on code in earlier chapters that uses features not available in Medley Common Lisp, so I had to skip those too.

Findings

Having completed the first pass over Practical Common Lisp, my initial impression is Medley's implementation of Common Lisp is capable and extensive. It can run with minor or no changes code that uses most basic and intermediate Common Lisp features.

The majority of the code I tried ran as expected. However, this work did reveal significant gaps and divergences from ANSI.

To account for the residential environment and other peculiarities of Medley, packages need to be defined in a specific way. For example, some common defpackage keyword arguments differ from ANSI. Also, uppercase strings seem to work better than keywords as package designators.

As for the gaps the loop iteration macro, symbol-macrolet, the #p reader macro, and other features turned out to be missing or not work.

While the incompatibilities with ANSI Common Lisp are relativaly easy to address or work around, what new users may find more difficult is understanding and using the residential environment of Medley.

Bringing Medley closer to ANSI Common Lisp

To plug the gaps this project uncovered Larry ported or implemented some of the missing features and fixed a few issues.

He ported a loop implementation which he's enhancing to add missing functionality like iterating over hash tables. Iterating over packages, which loop lacks at this time, is trickier. More work went into adding #p and an experimental symbol-macrolet.

Reviewing and merging the CLtL2 patches is still an open issue, a major project that involves substantial effort.

Future work and conclusion

When the new features are ready I'll do a second pass to check if more of the skipped code runs. Another outcome of the work may be the beginning of a test suite for Medley Common Lisp.

Regardless of the limitations, what the project highlighted is Medley is ready as a development environment for writing new Common Lisp code, or porting libraries and applications of small to medium complexity.

#CommonLisp #Interlisp #Lisp

Discuss... Email | Reply @amoroso@fosstodon.org

14 Apr 2024 10:51am GMT

02 Apr 2024

feedPlanet Lisp

Joe Marshall: You May Not Need That :around Method

I've seen this "anti-pattern" a few times in CLOS code. A superclass 'super will have a subclass 'sub and there will be a primary method specialized to the superclass.

(defmethod foo ((instance super) arg)
  (format t "~&Foo called on ~s." arg))

Then I'll see an :around method defined on the subclass:

(defmethod foo :around ((instance sub) arg)
  (format t "~&Start foo...~%")
  (call-next-method)
  (format t "~&End foo.~%"))

The intent here is clearly that code in the method specialized on the subclass is invoked "around" the call to the method specialized on the superclass.

But the :around qualifier is not necessary and probably doesn't do what is intended. If we remove the :around qualifier, then the most specific primary method will be the foo method specialized on 'sub. And the (call-next-method) invokation will chain up to the foo method specialized on 'super. It will work as was likely intended.

:around methods are useful when the superclass wants to run a method "around" the subclass. :around methods are combined from least specific to most specific - the opposite order of primary methods - so that the superclass can wrap the call to the subclass. An good example of where an :around method would be handy is when you need to sieze a lock around the call to the method. The superclass would sieze the lock in an :around method that would run before any of the subclass primary methods ran.

Ordinary chaining of methods doesn't need the :around qualifier. Just chain the methods.

02 Apr 2024 4:25am GMT

26 Mar 2024

feedPlanet Lisp

Joe Marshall: With- vs. call-with-

In Common Lisp, there are a lot of macros that begin with the word "with-". These typically wrap a body of code, and establish a context around the execution of the code.

In Scheme, they instead have a lot of functions that begin with the words "call-with-". They typically take a thunk or receiver as an argument, and establish a context around a call to the thunk or receiver.

Both of these forms accomplish the same sort of thing: running some user supplied code within a context. The Scheme way accomplishes this without a macro, but "call-with-" functions are rarely used as arguments to higher order functions. Writing one as a function is slightly easier than writing one as a macro because the compiler takes care of avoiding variable capture. Writing one as a macro leaves it up to the implementor to use appropriate gensyms. Writing one as a macro avoids a closure and a function call, but so does inlining the function. The macro form is slightly more concise because it doesn't have a lambda at every call site. The function form will likely be easier to debug because it will probably involve a frame on the stack.

There's no need to commit to either. Just write a "with-" macro that expands into a call to an inline "call-with-" function. This should equally please and irritate everyone.

26 Mar 2024 11:04am GMT

21 Mar 2024

feedPlanet Lisp

Joe Marshall: Porting a Game from Java (update)

I didn't expect anyone would be interested, so I just pushed the code that I had with little thought about anyone trying to use it. It turns out that some people actually wanted to run it, so I polished off some of the rough edges and made it easier to get working. Feel free to email me if you have questions or suggestions.

21 Mar 2024 5:08pm GMT

Eugene Zaikonnikov: EURISKO lives

When I wrote about EURISKO a few years before there hardly was an expectation of a follow-up. The system was a dusty legend with some cynical minds arguing whether it existed in the first place.

However, Lenat's death in August last year has unlocked his SAILDART archives account. This has led to a thrilling discovery of both AM and EURISKO sources by WhiteFlame. In a further development, seveno4 has managed to adapt EURISKO to run on Medley Interlisp.

While I marveled at the idea of discovery systems before I hadn't even considered ever running EURISKO myself as a possibility. Truly an Indiana Jones finding the Lost Ark moment. Yet this very low probability event has indeed happened, as documented in the video below. Rewind to 8:20 for the Medley run.

21 Mar 2024 3:00pm GMT

19 Mar 2024

feedPlanet Lisp

Joe Marshall: Porting a Game from Java

I decided to learn about games, so I followed along with a tutorial by Kaarin Gaming. His tutorial was written in Java, but of course I used Common Lisp. I made no attempt to faithfully replicate his design, but I followed it closely in some places, less so in others. The resulting program was more or less a port of the Java program to Common Lisp, so it not very remarkable in and of itself. Certainly I don't expect many people to be interested in reading beyond this point.

It's known that Java is wordy language and it shows in the end result. The tutorial had 3712 lines of Java code in 39 files and the equivalent Common Lisp was 2255 lines in 21 files. A typical Common Lisp file would contain more code than a typical Java file. It was often the case that a Common Lisp file would contain multiple classes.

Both versions used separate render and game mechanics threads. The render thread ran at about 60 frames per second where the game mechanics ran at 200 steps per second. The threads were mostly independent, but the game mechanics would occasionally have to query the render thread to find out whether an animation had completed or what frame the animation was on in order to synchronize attacks with reactions.

There were a couple of notable differences in the two implementations. The Java implementation would advance animation frames imperatively by incrementing the animation frame counter every few rendering cycles. The Common Lisp implementation would instead compute the animation frame functionally by subtracting the current time from the animation start time and dividing by the ticks per animation frame. In Common Lisp, different animation effects could be achieved by changing how the animation frame was computed. If you computed the frame number modulo the number of frames in the animation, you'd get an animation loop. If you clamped the frame number, you get a one-shot animation.

CLOS made some things easier. An :after method on the (setf get-state) of an entity would set the animation. The get-y method on some objects would (call-next-method) to get the actual y position and then add a time varying offset to make the object "float" in mid air. The get-x method on projectiles would would (call-next-method) to get the starting x position and then add a factor of the current ticks. This causes projectiles to travel uniformly horizontally across the screen. I often used the ability of CLOS to specialize on some argument other than the first one just to make the methods more readable.

The Common Lisp code is at http://github.com/jrm-code-project/Platformer, while the Java code is at https://github.com/KaarinGaming/PlatformerTutorial. Being a simple port of the Java code, the Common Lisp code is not exemplary, and since I didn't know what I was doing, it is kludgy in places. The Common Lisp code would be improved by a rewrite, but I'm not going to. I was unable to find a sound library for Common Lisp that could play .wav files without a noticable delay, so adding sound effects to the game is sort of a non-starter. I think I've gotten what I can out of this exercise, so I'll likely abandon it now.

19 Mar 2024 5:10pm GMT

14 Mar 2024

feedPlanet Lisp

vindarel: Oh no, I started a Magit-like plugin for the Lem editor

Lem is an awesome project. It's an editor buit in Common Lisp, ready to use out of the box for Common Lisp, that supports more languages and modes (Python, Rust, Elixir, Go, JavaScript, TypeScript, Haskell, Java, Nim, Dart, OCaml, Scala, Swift, shell, asm, but also markdown, ascii, JSON, HTML and CSS, SQL...) thanks to, in part, its built-in LSP support.

I took the challenge to add an interactive interface for Git, à la Magit, because you know, despite all its features (good vim mode, project-aware commands, grep, file tree view and directory mode, multiple cursors, tabs...), there's so much an editor should do to be useful all day long.

Now, for a Git project (and to a lower extent, Fossil and Mercurial ones) we can see its status, stage changes, commit, push & pull, start an interactive rebase...

I like the shape it is taking, and frankly, what I have been able to assemble in a continuously successful hack session is a tribute to what @cxxxr and the early contributors built. Lem's codebase is easily explorable (more so in Lem itself of course, think Emacs in steroïds with greater Common Lisp power), clear, and fun. Come to the Discord or watch the repository and see how new contributors easily add new features.

I didn't even have to build an UI interface, fortunately. I started with the built-in interactive grep mode, and built from there.

Enough talk, what can we do with Lem/legit as of today? After that, we'll discuss some implementation details.

Disclaimer: there's room for collaboration ;)

Table of Contents

Lem/legit - manual

NOTE: you'd better read the latest manual on Lem's repository: https://github.com/lem-project/lem/blob/main/extensions/legit/README.md

legit's main focus is to support Git operations, but it also has preliminary support for other VCSs (Fossil, Mercurial).

We can currently open a status window, stage and unstage files or diff hunks, commit our changes or again start an interactive rebase.

Its main source of inspiration is, obviously, Magit.

Status

legit is in development. It isn't finished nor complete nor at feature parity with Magit nor suitable for mission-critical work. Use at your own risk.

However it should run a few operations smoothly.

Load

legit is built into Lem but it isn't loaded by default. To load it, open a Lisp REPL (M-x start-lisp-repl) or evaluate Lisp code (M-:) and type:

(ql:quickload "lem/legit")

Now you can start it with C-x g or M-x legit-status.

Help

Press ? or C-x ? to call legit-help.

M-x legit-status

The status windows show us, on the left:

It also warns us if a rebase is in process.

and the window on the right shows us the file diffs or the commits' content.

Refresh the status content with g.

Navigation

We can navigate inside legit windows with n, p, M-n and M-p (go to next/previous section).

To change windows, use the usual M-o key from Lem.

Quit with q or C-x 0 (zero).

Stage or unstage files, diff hunks (s, u)

Stage changes with "s".

When your cursor is on an Unstaged change file, you can see the file changes on the right, and you can stage the whole file with s.

You can also go to the diff window on the right, navigate the diff hunks with n and p and stage a hunk with s.

Unstage a change with u.

Discard changes to a file

Use k. Be careful, you can loose your changes.

Commit

Pressing c opens a new buffer where you can write your commit message.

Validate with C-c C-c and quit with M-q (or C-c C-k).

Branches, push, pull

Checkout a branch with b b ("b" followed by another "b").

Create a new branch with b c.

You can push to the current remote branch with P p and pull changes (fetch) with F p.

NOTE: after pressing "P" or "F", you will not see an intermediate window giving you choices. Just press "P p" one after the other.

Interactive rebase

You can start a Git interactive rebase. Place the cursor on a commit you want to rebase from, and press r i.

You will be dropped into the classical Git rebase file, that presents you commits and an action to apply on them: pick the commit, drop it, fixup, edit, reword, squash...

For example:

pick 26b3990f the following commit
pick 499ba39d some commit

# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

legit binds keys to the rebase actions:

and so on.

Validate anytime with C-c C-c and abort with C-c C-k.

NOTE: at the time of writing, "reword" and "edit" are not supported.
NOTE: the interactive rebase is currently Unix only. This is due to the short shell script we use to control the Git process. Come join us if you know how to "trap some-fn SIGTERM" for Windows plateforms.

Abort, continue, skip

In any legit window, type r a to abort a rebase process (if it was started by you inside Lem or by another process), r c to call git rebase --continue and r s to call git rebase --skip.

Fossil

We have basic Fossil support: see current branch, add change, commit.

Mercurial

We have basic Mercurial support.

Customization

In the lem/porcelain package:

=> you can change the default call to the git binary.

Same with *fossil-base-args* and *hg-base-arglist* (oops, a name mismatch).

If a project is managed by more than one VCS, legit takes the first VCS defined in *vcs-existence-order*:

(defvar *vcs-existence-order*
  '(
    git-project-p
    fossil-project-p
    hg-project-p
    ))

where these symbols are functions with no arguments that return two values: a truthy value if the current project is considered a Git/Fossil/Mercurial project, and a keyword representing the VCS: :git, :fossil, :hg.

For example:

(defun hg-project-p ()
  "Return t if we find a .hg/ directory in the current directory (which should be the project root. Use `lem/legit::with-current-project`)."
  (values (uiop:directory-exists-p ".hg")
          :hg))

Variables and parameters in the lem/legit package. They might not be exported.

=> to help debugging

see sources in /extensions/legit/

Implementation details

Calls

Repository data is retrieved with calls to the VCS binary. We have a POC to read some data directly from the Git objects (proactively looking for best efficiency) using cl-git.

Basically, we get Git status data with git status --porcelain=v1. This outputs something like:

 A project/settings.lisp
 M project/api.lisp
?? project/search/datasources

we output this a to a string and we parse it.

Interactive rebase

The interactive rebase currently uses a Unix-only shell script.

When you run git rebase --interactive, the Git program creates a special file in .git/rebase-merge/git-rebase-merge-todo, opens it with your $EDITOR in the terminal, lets you edit it (change a "pick" to "fixup", "reword" etc), and on exit it interprets the file and runs the required Git operations. What we want is to not use Git's default program, edit the file with Lem and our special Legit mode that binds keys for quick actions (press "f" for "fixup" etc). So we bind the shell's $EDITOR to a dummy editor, this shell script:

function ok {
    exit 0
}

trap ok SIGTERM
echo "dumbrebaseeditor_pid:$$"

while :
do
        sleep 0.1
done

This script doesn't simulate an editor, it waits, so we can edit the rebase file with Lem, but this script catches a SIGTERM signal and exits successfully, so git-rebase is happy and terminates the rebase and all is well.

But that's Unix only.

On that matter Magit seems to be doing black magic.

Displaying data, actionable links

The basic function to write content to a buffer is

(insert-string point s :read-only t)

And this is how you make actionable links:

(put-text-property start end :visit-file-function function))

where :visit-file-function is any keyword you want, and function is any lambda function you want. So, how to make any link useful? Create a lambda, make it close over any variables you want, "store" it in a link, and later on read the attribute at point with

(text-property-at point :visit-file-function)

where point can be (buffer-point (window-buffer *my-window*)) for instance.

Now create a mode, add keybindings and you're ready to go.

;; Legit diff mode: view diffs.
;; We use the existing patch-mode and supercharge it with our keys.
(define-major-mode legit-diff-mode lem-patch-mode:patch-mode
    (:name "legit-diff"
     :syntax-table lem-patch-mode::*patch-syntax-table*
     :keymap *legit-diff-mode-keymap*)
  (setf (variable-value 'enable-syntax-highlight) t))

;; git commands.
;; Some are defined on peek-legit too.
(define-key *global-keymap* "C-x g" 'legit-status)

or a minor mode:

(define-minor-mode peek-legit-mode
    (:name "Peek"
     :keymap *peek-legit-keymap*)
  (setf (not-switchable-buffer-p (current-buffer)) t))

;; Git commands
;; Some are defined on legit.lisp for this keymap too.
(define-key *peek-legit-keymap* "s" 'peek-legit-stage-file)
(define-key *peek-legit-keymap* "u" 'peek-legit-unstage-file)
(define-key *peek-legit-keymap* "k" 'peek-legit-discard-file)

TODOs

Much needs to be done, if only to have a better discoverable UX.

First:

and then:

Closing words

You'll be surprised by all Lem's features and how easy it is to add features.

I believe it doesn't make much sense to "port Magit to Lem". The UIs are different, the text displaying mechanism is different, etc. It's faster to re-implement the required functionality, without the cruft. And look, I started, it's possible.

But, sad me, I didn't plan to be involved in yet another side project, as cool and motivating as it might be :S


14 Mar 2024 12:11pm GMT