From 619dc31dfc565a2f8d76b6ab701b64394506f5ae Mon Sep 17 00:00:00 2001 From: "Kartik K. Agaram" Date: Wed, 20 Oct 2021 11:51:24 -0700 Subject: sketching out a slow tutorial --- tutorial/add2.mu | 19 +++++ tutorial/c2f.mu | 19 +++++ tutorial/converter.mu | 138 +++++++++++++++++++++++++++++++++ tutorial/converter2.mu | 161 +++++++++++++++++++++++++++++++++++++++ tutorial/counter.mu | 35 +++++++++ tutorial/index.md | 169 +++++++++++++++++++++++++++++++++++++++++ tutorial/task1.png | Bin 0 -> 10546 bytes tutorial/task2.mu | 7 ++ tutorial/task2.png | Bin 0 -> 10513 bytes tutorial/task3.png | Bin 0 -> 94644 bytes tutorial/task4-calculator.html | 15 ++++ tutorial/task4-initial.png | Bin 0 -> 10950 bytes tutorial/task4-solution.mu | 15 ++++ tutorial/task4.mu | 15 ++++ tutorial/vimrc.vim | 2 + 15 files changed, 595 insertions(+) create mode 100644 tutorial/add2.mu create mode 100644 tutorial/c2f.mu create mode 100644 tutorial/converter.mu create mode 100644 tutorial/converter2.mu create mode 100644 tutorial/counter.mu create mode 100644 tutorial/index.md create mode 100644 tutorial/task1.png create mode 100644 tutorial/task2.mu create mode 100644 tutorial/task2.png create mode 100644 tutorial/task3.png create mode 100644 tutorial/task4-calculator.html create mode 100644 tutorial/task4-initial.png create mode 100644 tutorial/task4-solution.mu create mode 100644 tutorial/task4.mu create mode 100644 tutorial/vimrc.vim diff --git a/tutorial/add2.mu b/tutorial/add2.mu new file mode 100644 index 00000000..250d72ca --- /dev/null +++ b/tutorial/add2.mu @@ -0,0 +1,19 @@ +fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) { + var result/eax: int <- do-add 3, 4 + draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen screen, result, 3/fg=cyan 0/bg +} + +fn do-add a: int, b: int -> _/eax: int { + var result/eax: int <- copy a + result <- add b + return result +} + +fn test-do-add { + var observed/eax: int <- do-add 0, 0 + check-ints-equal observed, 0, "F - 0+0" + observed <- do-add 3, 0 + check-ints-equal observed, 3, "F - 3+0" + observed <- do-add 3, 2 + check-ints-equal observed, 5, "F - 3+2" +} diff --git a/tutorial/c2f.mu b/tutorial/c2f.mu new file mode 100644 index 00000000..250d72ca --- /dev/null +++ b/tutorial/c2f.mu @@ -0,0 +1,19 @@ +fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) { + var result/eax: int <- do-add 3, 4 + draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen screen, result, 3/fg=cyan 0/bg +} + +fn do-add a: int, b: int -> _/eax: int { + var result/eax: int <- copy a + result <- add b + return result +} + +fn test-do-add { + var observed/eax: int <- do-add 0, 0 + check-ints-equal observed, 0, "F - 0+0" + observed <- do-add 3, 0 + check-ints-equal observed, 3, "F - 3+0" + observed <- do-add 3, 2 + check-ints-equal observed, 5, "F - 3+2" +} diff --git a/tutorial/converter.mu b/tutorial/converter.mu new file mode 100644 index 00000000..a8aa26e3 --- /dev/null +++ b/tutorial/converter.mu @@ -0,0 +1,138 @@ +# Temperature Converter app +# https://eugenkiss.github.io/7guis/tasks/#temp +# +# To build: +# $ ./translate converter.mu +# To run: +# $ qemu-system-i386 code.img + +# todo: +# less duplication +# error checking for input without hard-aborting + +fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) { + # celsius numeric representation + var zero: float + var celsius/xmm1: float <- fahrenheit-to-celsius zero + # celsius string representation + var s-storage: (stream byte 0x10) + var s/ecx: (addr stream byte) <- address s-storage + write-float-decimal-approximate s, celsius, 2/decimal-places + # celsius input/display + var celsius-input-storage: gap-buffer + var celsius-input/esi: (addr gap-buffer) <- address celsius-input-storage + initialize-gap-buffer celsius-input, 8/capacity + load-gap-buffer-from-stream celsius-input, s + var cursor-in-celsius?/edx: boolean <- copy 0xffffffff/true + # fahrenheit numeric representation + var fahrenheit/xmm2: float <- celsius-to-fahrenheit celsius + # fahrenheit string representation + clear-stream s + write-float-decimal-approximate s, fahrenheit, 2/decimal-places + # fahrenheit input/display + var fahrenheit-input-storage: gap-buffer + var fahrenheit-input/edi: (addr gap-buffer) <- address fahrenheit-input-storage + initialize-gap-buffer fahrenheit-input, 8/capacity + load-gap-buffer-from-stream fahrenheit-input, s + var cursor-in-fahrenheit?/ebx: boolean <- copy 0/false # exactly one cursor boolean must be true at any time + # widget title + set-cursor-position screen, 0x1f/x 0xe/y + draw-text-rightward-from-cursor-over-full-screen screen, " Converter ", 0xf/fg 0x16/bg + # event loop + { + # draw current state to screen + clear-rect screen, 0x1f/xmin 0xf/ymin, 0x45/xmax 0x14/ymax, 0xc5/color + var x/eax: int <- render-gap-buffer screen, celsius-input, 0x20/x 0x10/y, cursor-in-celsius?, 7/fg 0/bg + x <- draw-text-rightward screen, " celsius = ", x, 0x45/xmax, 0x10/y, 7/fg 0xc5/bg + x <- render-gap-buffer screen, fahrenheit-input, x 0x10/y, cursor-in-fahrenheit?, 7/fg 0/bg + x <- draw-text-rightward screen, " fahrenheit", x, 0x45/xmax, 0x10/y, 7/fg 0xc5/bg + # render a menu bar + set-cursor-position screen, 0x21/x 0x12/y + draw-text-rightward-from-cursor-over-full-screen screen, " tab ", 0/fg 0x5c/bg=highlight + draw-text-rightward-from-cursor-over-full-screen screen, " switch sides ", 7/fg 0xc5/bg + draw-text-rightward-from-cursor-over-full-screen screen, " enter ", 0/fg 0x5c/bg=highlight + draw-text-rightward-from-cursor-over-full-screen screen, " convert ", 7/fg 0xc5/bg + # process a single keystroke + $main:input: { + var key/eax: byte <- read-key keyboard + var key/eax: grapheme <- copy key + compare key, 0 + loop-if-= + # tab = switch cursor between input areas + compare key, 9/tab + { + break-if-!= + cursor-in-celsius? <- not + cursor-in-fahrenheit? <- not + break $main:input + } + # enter = convert in appropriate direction + compare key, 0xa/newline + { + break-if-!= + { + compare cursor-in-celsius?, 0/false + break-if-= + clear-stream s + emit-gap-buffer celsius-input, s + celsius <- parse-float-decimal s + fahrenheit <- celsius-to-fahrenheit celsius + clear-stream s + write-float-decimal-approximate s, fahrenheit, 2/decimal-places + clear-gap-buffer fahrenheit-input + load-gap-buffer-from-stream fahrenheit-input, s + } + { + compare cursor-in-fahrenheit?, 0/false + break-if-= + clear-stream s + emit-gap-buffer fahrenheit-input, s + { + var tmp/xmm1: float <- parse-float-decimal s + fahrenheit <- copy tmp + } + celsius <- fahrenheit-to-celsius fahrenheit + clear-stream s + write-float-decimal-approximate s, celsius, 2/decimal-places + clear-gap-buffer celsius-input + load-gap-buffer-from-stream celsius-input, s + } + break $main:input + } + # otherwise pass key to appropriate input area + compare cursor-in-celsius?, 0/false + { + break-if-= + edit-gap-buffer celsius-input, key + break $main:input + } + compare cursor-in-fahrenheit?, 0/false + { + break-if-= + edit-gap-buffer fahrenheit-input, key + break $main:input + } + } + loop + } +} + +fn fahrenheit-to-celsius f: float -> _/xmm1: float { + var result/xmm1: float <- copy f + var thirty-two/eax: int <- copy 0x20 + var thirty-two-f/xmm0: float <- convert thirty-two + result <- subtract thirty-two-f + var factor/xmm0: float <- rational 5, 9 + result <- multiply factor + return result +} + +fn celsius-to-fahrenheit c: float -> _/xmm2: float { + var result/xmm1: float <- copy c + var factor/xmm0: float <- rational 9, 5 + result <- multiply factor + var thirty-two/eax: int <- copy 0x20 + var thirty-two-f/xmm0: float <- convert thirty-two + result <- add thirty-two-f + return result +} diff --git a/tutorial/converter2.mu b/tutorial/converter2.mu new file mode 100644 index 00000000..c5dde9f5 --- /dev/null +++ b/tutorial/converter2.mu @@ -0,0 +1,161 @@ +# Temperature Converter app +# https://eugenkiss.github.io/7guis/tasks/#temp +# +# To build: +# $ ./translate converter2.mu +# To run: +# $ qemu-system-i386 code.img + +# todo: +# error checking for input without hard-aborting + +fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) { + # imgui approach + forever { + number-input fahrenheit, cursor-in-fahrenheit? + number-input celsius, cursor-in-celsius? + if (menu-key 9/tab "Tab" "switch sides") { # requires non-blocking input + cursor-in-celsius? <- not + cursor-in-fahrenheit? <- not + } + if (menu-key 0xa/newline "Enter" "convert") { + if cursor-in-fahrenheit + celsius = fahrenheit-to-celsius fahrenheit + else + fahrenheit = celsius-to-fahrenheit celsius + } + } + # celsius numeric representation + var zero: float + var celsius/xmm1: float <- fahrenheit-to-celsius zero + # celsius input/display + var celsius-input-storage: gap-buffer + var celsius-input/esi: (addr gap-buffer) <- address celsius-input-storage + initialize-gap-buffer celsius-input, 8/capacity + value-to-input celsius, celsius-input + # fahrenheit numeric representation + var fahrenheit/xmm2: float <- celsius-to-fahrenheit celsius + # fahrenheit input/display + var fahrenheit-input-storage: gap-buffer + var fahrenheit-input/edi: (addr gap-buffer) <- address fahrenheit-input-storage + initialize-gap-buffer fahrenheit-input, 8/capacity + value-to-input fahrenheit, fahrenheit-input + # cursor toggle + var cursor-in-celsius?/edx: boolean <- copy 0xffffffff/true + # + render-title screen + # event loop + { + # render + render-state celsius-input, fahrenheit-input, cursor-in-celsius? + render-menu-bar screen + # process a single keystroke + $main:input: { + var key/eax: byte <- read-key keyboard + var key/eax: grapheme <- copy key + compare key, 0 + loop-if-= + # tab = switch cursor between input areas + compare key, 9/tab + { + break-if-!= + cursor-in-celsius? <- not + break $main:input + } + # enter = convert in appropriate direction + compare key, 0xa/newline + { + break-if-!= + { + compare cursor-in-celsius?, 0/false + break-if-= + var tmp/xmm0: float <- input-to-value celsius-input + celsius <- copy tmp + fahrenheit <- celsius-to-fahrenheit celsius + value-to-input fahrenheit, fahrenheit-input + break $main:input + } + var tmp/xmm0: float <- input-to-value fahrenheit-input + fahrenheit <- copy tmp + celsius <- fahrenheit-to-celsius fahrenheit + value-to-input celsius, celsius-input + break $main:input + } + # otherwise pass key to appropriate input area + compare cursor-in-celsius?, 0/false + { + break-if-= + edit-gap-buffer celsius-input, key + break $main:input + } + edit-gap-buffer fahrenheit-input, key + } + loop + } +} + +# business logic + +fn fahrenheit-to-celsius f: float -> _/xmm1: float { + var result/xmm1: float <- copy f + var thirty-two/eax: int <- copy 0x20 + var thirty-two-f/xmm0: float <- convert thirty-two + result <- subtract thirty-two-f + var factor/xmm0: float <- rational 5, 9 + result <- multiply factor + return result +} + +fn celsius-to-fahrenheit c: float -> _/xmm2: float { + var result/xmm1: float <- copy c + var factor/xmm0: float <- rational 9, 5 + result <- multiply factor + var thirty-two/eax: int <- copy 0x20 + var thirty-two-f/xmm0: float <- convert thirty-two + result <- add thirty-two-f + return result +} + +# helpers for UI + +fn input-to-value in: (addr gap-buffer) -> _/xmm0: float { + var s-storage: (stream byte 0x10) + var s/ecx: (addr stream byte) <- address s-storage + emit-gap-buffer in, s + var result/xmm1: float <- parse-float-decimal s + return result +} + +fn value-to-input in: float, out: (addr gap-buffer) { + var s-storage: (stream byte 0x10) + var s/ecx: (addr stream byte) <- address s-storage + write-float-decimal-approximate s, in, 2/decimal-places + clear-gap-buffer out + load-gap-buffer-from-stream out, s +} + +# helpers for rendering to screen +# magic constants here need to be consistent between functions + +fn render-title screen: (addr screen) { + set-cursor-position screen, 0x1f/x 0xe/y + draw-text-rightward-from-cursor-over-full-screen screen, " Converter ", 0xf/fg 0x16/bg +} + +fn render-state screen: (addr screen), c: (addr gap-buffer), f: (addr gap-buffer), cursor-in-c?: boolean { + clear-rect screen, 0x1f/xmin 0xf/ymin, 0x45/xmax 0x14/ymax, 0xc5/color + var x/eax: int <- render-gap-buffer screen, c, 0x20/x 0x10/y, cursor-in-c?, 7/fg 0/bg + x <- draw-text-rightward screen, " celsius = ", x, 0x45/xmax, 0x10/y, 7/fg 0xc5/bg + var cursor-in-f?/ecx: boolean <- copy cursor-in-c? + cursor-in-f? <- not + x <- render-gap-buffer screen, f, x 0x10/y, cursor-in-f?, 7/fg 0/bg + x <- draw-text-rightward screen, " fahrenheit", x, 0x45/xmax, 0x10/y, 7/fg 0xc5/bg +} + +fn render-menu-bar screen: (addr screen) { + set-cursor-position screen, 0x21/x 0x12/y + draw-text-rightward-from-cursor-over-full-screen screen, " tab ", 0/fg 0x5c/bg=highlight + draw-text-rightward-from-cursor-over-full-screen screen, " switch sides ", 7/fg 0xc5/bg + draw-text-rightward-from-cursor-over-full-screen screen, " enter ", 0/fg 0x5c/bg=highlight + draw-text-rightward-from-cursor-over-full-screen screen, " convert ", 7/fg 0xc5/bg +} diff --git a/tutorial/counter.mu b/tutorial/counter.mu new file mode 100644 index 00000000..b1f3cc42 --- /dev/null +++ b/tutorial/counter.mu @@ -0,0 +1,35 @@ +# Counter app +# https://eugenkiss.github.io/7guis/tasks/#counter +# +# To build: +# $ ./translate counter.mu +# To run: +# $ qemu-system-i386 code.img + +fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) { + var count/ecx: int <- copy 0 + # widget title + set-cursor-position screen, 0x1f/x 0xe/y + draw-text-rightward-from-cursor-over-full-screen screen, " Counter ", 0xf/fg 0x16/bg + # event loop + { + # draw current state to screen + clear-rect screen, 0x1f/xmin 0xf/ymin, 0x40/xmax 0x14/ymax, 0xc5/color + set-cursor-position screen, 0x20/x 0x10/y + draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen screen, count, 7/fg 0xc5/bg + # render a menu bar + set-cursor-position screen, 0x24/x 0x12/y + draw-text-rightward-from-cursor-over-full-screen screen, " enter ", 0/fg 0x5c/bg=highlight + draw-text-rightward-from-cursor-over-full-screen screen, " increment ", 7/fg 0xc5/bg + # process a single keystroke + { + var key/eax: byte <- read-key keyboard + compare key, 0 + loop-if-= + compare key, 0xa/newline + break-if-!= + count <- increment + } + loop + } +} diff --git a/tutorial/index.md b/tutorial/index.md new file mode 100644 index 00000000..9cbc5e9f --- /dev/null +++ b/tutorial/index.md @@ -0,0 +1,169 @@ +# A slow tour through Mu software on x86 computers + +[Mu](https://github.com/akkartik/mu) shrinks all the software in a computer +until it can (in principle) fit in a single head. Sensible error messages with +as little code as possible, starting all the way from your (x86) processor's +instruction set. Everything easy to change to your needs +([habitable](http://akkartik.name/post/habitability)), everything easy to +check up on ([auditable](http://akkartik.name/post/neighborhood)). + +This page is a guided tour through Mu's Readme and reference documentation. +We'll start out really slow and gradually accelerate as we build up skills. By +the end of it all, I hope you'll be able to program your processor to run some +small graphical programs. The programs will only use a small subset of your +computer's capabilities; there's still a lot I don't know and therefore cannot +teach. However, the programs will run on a _real_ processor without needing +any other intermediary software. + +_Prerequisites_ + +You will need: + +* A computer with an x86 processor running Linux. We're going to slowly escape + Linux, but we'll need it at the start. Mu works on other platforms, but be + warned that things will be _much_ (~20x) slower. +* Some fluency in typing commands at the terminal and interpreting their + output. +* Fluency with some text editor. Things like undo, copying and pasting text, + and saving work in files. A little experience programming in _some_ language + is also handy. +* [Git](https://git-scm.com) for version control. +* [QEMU](https://www.qemu.org) for emulating a processor without Linux. +* Basic knowledge of number bases, and the difference between decimal and + hexadecimal numbers. + +If you have trouble with any of this, [I'm always nearby and available to +answer questions](http://akkartik.name/contact). The prerequisites are just +things I haven't figured out how to explain yet. In particular, I want this +page to be accessible to people who are in the process of learning +programming, but I'm sure it isn't good enough yet for that. Ask me questions +and help me improve it. + +# Task 1: getting started + +Open a terminal and run the following commands to prepare Mu on your computer: + +``` +git clone https://github.com/akkartik/mu +cd mu +``` + +Run a small program to start: + +``` +./translate apps/ex5.mu +qemu-system-i386 code.img +``` + +If you aren't on Linux, the command for creating `code.img` will be slightly +different: + +``` +./translate_emulated apps/ex5.mu +qemu-system-i386 code.img +``` + +Either way, you should see this: + +screenshot of hello world on the Mu computer + +If you have any trouble at this point, don't waste _any_ time thinking about +it. Just [get in touch](http://akkartik.name/contact). + +(You can look at `apps/ex5.mu` at this point if you like. It's just a few +lines long. But don't worry if it doesn't make much sense.) + +# Task 2: running tests + +Here's a new program to run: + +``` +./translate tutorial/task2.mu +qemu-system-i386 code.img +``` + +(As before, I'll leave you to substitute `translate` with `translate_emulated` +if you're not on Linux.) + +This time the screen will look like this: + +screenshot of failing test on the Mu computer + +Each of the dots is a _test_, a little self-contained and automated program +run with an expected result. Mu comes with a lot of tests, and it always runs +all tests before it runs any program. You may have missed the dots when you +ran Task 1 because there were no failures. They were printed on the screen and +then immediately erased. In Task 2, however, we've deliberately included a +failing test. When any tests fail, Mu will immediately stop, showing you +messages from failing tests and implicitly asking you to first fix them. + +(Don't worry just yet about what the message in the middle of all the dots means.) + +# Task 3: configure your text editor + +So far we haven't used a text editor yet, but we will now be starting to do +so. Before we do, it's worth spending a little bit of time setting your +preferred editor up to be a little more ergonomic. Mu comes with _syntax +highlighting_ settings for a few common text editors in the `editor/` +sub-directory. If you don't see your text editor there, or if you don't know +what to do with those files, [get in touch!](http://akkartik.name/contact) +Here's what my editor (Vim) looks like with these settings on the program of +Task 1: + +Vim text editor rendering some colors in a Mu program + +It's particularly useful to highlight _comments_ which the computer ignores +(everything on a line after a `#` character) and _strings_ within `""` double +quotes. + +# Task 4: your first Mu statement + +Mu is a statement-oriented language. Read the first section of the [Mu syntax +description](https://github.com/akkartik/mu/blob/main/mu.md) to learn a little +bit about it. + +Here's a skeleton of a Mu function that's missing a single statement. + +``` +fn the-answer -> _/eax: int { + var result/eax: int <- copy 0 + # insert your statement below { + + # } + return result +} +``` + +Try running it now: +``` +./translate tutorial/task4.mu +qemu-system-i386 code.img +``` + +(As before, I'll leave you to substitute `translate` with `translate_emulated` +if you're not on Linux.) + +You should see a failing test that looks something like this: + +screenshot of the initial (failing) state of task 4 + +Open `tutorial/task4.mu` in your text editor. Think about how to add a line +between the `{}` lines to make `the-answer` return 42. Rerun the above +commands. You'll know you got it right all the tests pass, i.e. when the rows +of dots and text above are replaced by an empty screen. + +Don't be afraid to run the above commands over and over again as you try out +different solutions. Here's a way to run them together so they're easy to +repeat. + +``` +./translate tutorial/task4.mu && qemu-system-i386 code.img +``` + +In programming there is no penalty for making mistakes, and once you arrive at +the correct solution you have it forever. As always, [feel free to ping me and +ask questions or share your experience](http://akkartik.name/contact). + +One gotcha to keep in mind is that numbers in Mu must always be in hexadecimal +notation, starting with `0x`. Use a calculator on your computer or phone to +convert 42 to hexadecimal, or [this page on your web browser](http://akkartik.github.io/mu/tutorial/task4-calculator.html). diff --git a/tutorial/task1.png b/tutorial/task1.png new file mode 100644 index 00000000..0009990f Binary files /dev/null and b/tutorial/task1.png differ diff --git a/tutorial/task2.mu b/tutorial/task2.mu new file mode 100644 index 00000000..c31efc0f --- /dev/null +++ b/tutorial/task2.mu @@ -0,0 +1,7 @@ +fn test-1 { + check-ints-equal 1, 2, "F - test-1" +} + +fn main { + # do nothing +} diff --git a/tutorial/task2.png b/tutorial/task2.png new file mode 100644 index 00000000..6b87e727 Binary files /dev/null and b/tutorial/task2.png differ diff --git a/tutorial/task3.png b/tutorial/task3.png new file mode 100644 index 00000000..82b5f594 Binary files /dev/null and b/tutorial/task3.png differ diff --git a/tutorial/task4-calculator.html b/tutorial/task4-calculator.html new file mode 100644 index 00000000..dca1190c --- /dev/null +++ b/tutorial/task4-calculator.html @@ -0,0 +1,15 @@ + + +
+ = +
+ diff --git a/tutorial/task4-initial.png b/tutorial/task4-initial.png new file mode 100644 index 00000000..99b5514a Binary files /dev/null and b/tutorial/task4-initial.png differ diff --git a/tutorial/task4-solution.mu b/tutorial/task4-solution.mu new file mode 100644 index 00000000..060b7123 --- /dev/null +++ b/tutorial/task4-solution.mu @@ -0,0 +1,15 @@ +fn the-answer -> _/eax: int { + var result/eax: int <- copy 0 + # insert your statement below { + result <- copy 0x2a + # } + return result +} + +fn test-the-answer { + var result/eax: int <- the-answer + check-ints-equal result, 0x2a, "F - the-answer should return 42, but didn't." +} + +fn main { +} diff --git a/tutorial/task4.mu b/tutorial/task4.mu new file mode 100644 index 00000000..ca8da8f4 --- /dev/null +++ b/tutorial/task4.mu @@ -0,0 +1,15 @@ +fn the-answer -> _/eax: int { + var result/eax: int <- copy 0 + # insert your statement below { + + # } + return result +} + +fn test-the-answer { + var result/eax: int <- the-answer + check-ints-equal result, 0x2a, "F - the-answer should return 42, but didn't." +} + +fn main { +} diff --git a/tutorial/vimrc.vim b/tutorial/vimrc.vim new file mode 100644 index 00000000..348fe364 --- /dev/null +++ b/tutorial/vimrc.vim @@ -0,0 +1,2 @@ +" when opening files in this directory, load vimrc from cwd (top-level) +source vimrc.vim -- cgit 1.4.1-2-gfad0