Hello cyberpals! It's been almost a year since
my last video. I hope you are doing well.
I have not been doing too much Mac programming in the past year,
but I got kind of sidetracked with some hardware
projects, but I wanted
to start development of something lately
and I again ran into the problem that I don't have a good way of managing
my changes to code.
I know some other classic Mac developers are
using like Netatalk
on a Linux machine, and then like mounting
your drive over Localtalk and then just committing on
the Linux side with Git or something after you convert
all the new lines. But I didn't really want to go that route,
and I already have the diff tool that I ported in
a previous video, so I thought I could just expand
on that and make it into something that can store
diffs that it generates and then kind of just stack them.
So the way that Git works
is that it stores every revision of every file
in the .git directory, and it uses some compression
and stuff. But if you have, like, a ten meg binary file and you append
one byte to it and then commit that, your
.git directory is 20 megabytes because it has two copies of that ten
megabyte file, whereas RCS,
which is what CVS is based on, would just store the
delta between the two files, so it would be ten megabytes
plus some very small overhead. So I figured
I'd go that route because disk space is not very big on
Mac OS around these classic Macs,
so I could just store a diff between each version.
And then in the case that
you want to check out a specific version of a
file, you could just start at the first one, apply that diff,
apply the second diff, and so on until you get the
file at that version.
And so I would only need to store the latest version of the file
in the repository file,
or like a little database or something.
And then that way, when you want to do a diff of what's on the
disk, it would have to just fetch that last
version of the file and then do a diff between that file and the
one that's on the disk. So over
the past, like, two weeks I wrote such a thing.
I'm calling it Amend because you're
amending your software.
And the way that it works is you get this.
Well, when you first run it, it asks you to open an existing repo.
So I'll just show you the repo that I have for the Amend code
itself.
So you get this GUI. These are all the commits. These are all the
files in the repository that it knows about,
and then you can view a
specific commit,
and it would show you the diff of that commit.
It's not generating a diff in real time,
like Git would. If you did `git show`
or something. So it's just storing the actual diff. So like
that lag in viewing sizable
diff is just the computation of fetching
it out of the file and showing it here.
So like the initial import of
all the files because I obviously couldn't import the code as
I was writing it because I had to build up the code until it
was at the point where I could actually do
stuff with the files. So this is a quite large diff,
but it is able to handle it if you have the
memory. This is a Mac Plus, so it has four megabytes of
memory. Anyway, so that's kind of
the tool and you can
generate a diff of the latest copy of each file that
it has versus what's on the disk. And then it generates
a diff, shows you the diff and gives you a log window to
commit it. You can add files to the repo,
and then the first time that you would generate a diff of those new files,
it does it like this where every
line is basically new.
And then if you were to delete a file later, it just does the reverse.
So every line is gone. And then if the resulting file size is
zero, it would assume that you meant to delete it,
which I think is how CVS handles that.
I guess it would use the Attic, but anyway, and then it
might actually be easier if I just show you like a demo of this.
So I will create a text file,
and currently it only operates on text files. It doesn't handle binary yet
because-- you can't add the resource files to
your repo, and I'll probably
handle that later because it's
not useful to view a diff of a binary change.
So I may have to do something clever there
to store the diff, but not show it to you.
And just say like these two binary files differ.
So let me just create a test file and
we'll put that in test.
Okay. So you would run Amend. It asks you if you want to open
a repo, you just cancel and then create a new one,
and it
has to be in the directory of where
all your files are. Just like your .git
directory is like the root of your tree.
So you create a new repo. There's no files,
so you will add one,
and then you can generate a diff.
It's going to happen quickly because there's nothing there or it's a new
file,
and then you commit and it stores the diff and
the latest version of the file. And then you can view that file,
and then we can change
the file and say,
commit it.
And the way that this works--
let me add a second file to make this clearer.
The way that RCS and CVS work is on individual files.
So every time you change multiple files at once and they're part
of a related change set, CVS and RCS just
write that log message into every file that
it's tracking, whereas with Git, it has
a notion of, like, a changeset. So it bundles the
same commit across multiple files as one commit with
one log message. And so Amend
operates on change sets, not individual files, which I
find more useful.
Let me change that file
first one need
to implement that. That's weird. I think that's like a
MultiFinder hook that you have to listen for.
Okay, so now it should add the second file,
and then diif
the first one.
Okay. So when you view a commit and it
shows you the diff when you narrow the file list down,
it only shows you the it narrows the commit list down to
the commits that reference each file, which I think is
kind of neat. And then by default, you're just operating in
all files because you're
kind of viewing the whole progress of
your project. And then if you made like,
a mistake and you want to revert this,
you can just revert changes instead of committing
them. So if you wanted to take this back to
file, you could just
say second file and then revert,
and then it asks where you want to save it.
And then this
is back to "file". So that's pretty much the tool.
It's been pretty useful for me in my development of
the tool. Now that it's at a kind of a stable point
where it can actually track changes, and I can view
them and commit whenever I'm done
with a particular change across multiple files.
So in the development for this I kind of encountered
a bunch of new things that I have not done
before. I made a list
of them here. So the way that all this works is that
the repo file is actually just a
resource file because Mac
OS like classic Mac OS, and it has all this stuff baked into the
ROM for handling resource files. And all these things are,
like, heavily optimized because they're all handwritten in
assembly and stuff. So I figured why not use those
routines and just treat the file like a resource
file instead of making my own binary format
on disk? So the way that it works is when you create a
new repo, it copies this template out
of the resource file of the tool itself.
And this template file tells ResEdit how
to view and parse each of the custom resource
types that I made. So like, it has a template for
a commit which says that there's
a date and it's a long integer.
There's an author, it's a Pascal string, number of files,
and then so on. And then with this template in the resource
file, when you open a resource of that type
that looks weird.
ResEdit knows how to parse this, and it's
not able to distinguish between signed and unsigned longs. Anyway.
So these are all the file IDs and then the log, and so it's able
to view all of these types of these
resources.
And so you have the [type] and the creator. So when you revert the file
and then have to store it back on disk and it will create the new
file with this type and that creator.
So that's kind of cool. So I started working on
my own binary file format, and then I figured this is kind of
like a little database, so there's probably a better way to do this.
So that's worked out pretty well.
The other major thing that I had to implement is--
the TextEdit control.
The TextEdit controls on Mac OS are
limited to 32K of
text, which seems like a lot. But when you're viewing like a giant diff of
a whole bunch of files, you can actually reach 32K somewhat
easily. So then the question is like,
can you show more than 32K in the TextEdit control?
And I figured you don't really need to because you're just viewing a giant diff,
and it's not really that helpful to view that much code.
You're basically just going to generate the diff and then commit it.
But the thing is, when you generate the diff as the
diff program or the diff function runs and
calls back into the function that I wrote that adds
a new line to the TextEdit control.
Once that finishes, and then you commit the diff,
it just stores that resource that contains
all of that diff text. It just stores that in the resource file,
so you don't have two copies of it, basically. Like, you don't have to store
it in your own big giant buffer
and also show like, a 32K version in the TextEdit
control. And then it commits the first one. So when
it's loading
text into the TextEdit control, it checks if the checks
if the-- oh, that was kind of accurate-- amount of text that is added is
32K, and if it is, it just stops inserting text. But it expands
the handle, the internal handle that
the TextEdit control uses, like under the hood,
and it expands that and then just starts adding text to that handle
on its own. And then because the TextEdit control
hasn't been notified, that that's enlarged and that there's more text in
there, it doesn't try to do anything with that extra text. It just never shows
it because it's like internal
variable that says how much text it has has not changed. And then
when I go to commit it, it just attaches to this handle and
is able to read the full size of that handle and
save it into the resource file. So it can handle more than 32K
of diffs, because it's important because when you're
stacking diffs later, you need the full text of the diff,
even if you're not able to view it. So that was one thing that
wasn't too big of a challenge. But I
wanted to try and do this as memory efficient as possible,
because it should run on machines with less than four megabytes of
memory. The other thing is that I kind of
use a newer style of handling state
between because there's two windows. Now there's a browser
window that shows you all the commits and the files, and then the committer window
pops up once you are generating a diff and then
committing. So I needed to track the state a
bit differently from the main event handler loop.
So the way that it-- this all kind of looks the same.
But what it does is when
you get a null event, it calls this browser_idle
function, and then the browser_idle function just
looks at what state it's in and it knows to do something.
And the reason that I did this is because when
you try and change the view
outside of these update events, it doesn't
draw properly, or it can draw twice and like flicker.
So the way that you need to do those is--
you don't want to draw at the wrong time. Anyway.
So the way that I'm doing that is if I want to update the file
list from something, I don't just have a function call this function. It just sets
the state to update file list and then waits until the
event handler loops around and is in a proper state where
it doesn't need to be redrawing anything at that moment. And then it can call
into this. It'll generate a null event in the loop
and then go into this function, and then I
can call this function to like regenerate the list or whatever.
And then the same thing when the committer window is open. If it
needs to like this actually initializes the window and shows
it. And then once the
committer window opens, it sets the state to this
changes the state so the window will come up,
draw properly, and then wait until everything is settled and it
gets back into generates like a null idle event,
and then it will come back and then handle this new state
and then actually generate the desk and do whatever.
So that's another
thing is, I don't think for the diff program, I ended up generating
my own icon, but I did for this program.
And the way that you do that is if you
open up your resource file for your program,
there is a bundle resource,
and then the bundle resource says which
icon to show when you have the signature.
So that's like the creator code of your program,
which you set in...
So when you generate the program, when you tell THINK C to
Build Application, it sets
the creator and file type of binary
itself with the values that you put here. So it's your
own creator, and then a file type of application,
and then Finder-- when you copy that binary
to somewhere, Finder rebuilds its desktop file, and then it looks
for a bundle in that program with the same creator
that you set and the type of application. And then the
bundle says to use that icon, and then it
displays this icon and uses that mask.
And then the other thing is when your program creates files,
in the end, when you create a repo file,
I create those files with a creator of AMND, but a type
of AMRP for Amend repo. And then this tells Finder
which icon to use for those .repo files which
are in--
which is here. So that tells Finder which icon to display. So that
was something new, but not major.
But kind of related to that is when you
double click on a file that you created
from your application. The way that Finder
knows which application to launch is because
of that same bundle. So it will look for the
program with the creator of this and the type
of file, and it'll see which program
has a bundle with those two things. And then it will launch that program.
And then once you launch the program, you need to be able to see which
file was passed from Finder.
Which is this here you do
CountAppFiles and then the type and you
get account back. This type is either open or
print. Obviously, print is not useful here. So you do
CountAddFiles files and if you get a count, I'm doing that here.
It just gets those file name and
the vrefnum which
shows which folder is in. It calls
GetAppFiles, which gets that information and then clears it and
then opens that file instead of the
default action, which is to just give you a dialog saying which file you want
to open. So that was something else that was new.
Probably the biggest thing that took me the longest to figure out is--
something that I didn't do properly in diff is
when you view
a diff in a TextEdit control, there are
raw tab characters in
the text, but the TextEdit control does
not show those as text. And in the diff program in
the previous video I made it basically expand those tabs into
spaces so that it would show the
horizontal space properly. But because
Amend has to actually store those diffs, it can't convert
them into spaces. It needs to preserve the tab.
But then the question is, how do you show the
actual tab?
And the way that I did that is by implementing all of the
hooks the TextEdit provides so that you
can do custom widths and drawing
of each character, which is why this is a tad slower than
normal. But if you notice here like this here is
a tab and that character there
that is one tab, whereas in the
diff program it would just render that it's like four spaces.
And doing that was a lot of work, actually.
So this is two tabs and
the way that works is by
this. I put this in a separate module
in case anybody else wants to use it, because it's apparently like a
major thing to support tabs in TextEdit controls,
like in Think Reference, which I consulted a lot in
development of this,
there is some text in here
that says like the TextEdit doesn't support tabs.
Let me just see if I can find it somewhere. It's in one
of these articles, but it's basically like TextEdit still doesn't support
even after the styled TextEdit, which came in System 6 to
support like different languages
and right to left text and stuff like that.
It's basically saying even with these new styled TextEdit controls,
you still have no tab characters for it anyway.
So the way that this all works is that you set up a TextEdit
of control using the TEStylNew instead of just TENew.
And then you call
TETabEnable, which is in which I wrote. And this
adds these new hooks, these three hooks
for the text control, which is you want
to be called when you need to calculate the width of each
character. When you want to actually draw the
character into on the screen after
the TextEdit code has placed the pen in the right position and
set the font properly and all that stuff, then it hands it off
to you and says, okay, now you actually need to draw it. And then the
hit test is like if you click inside
of a character like here,
your hit test function needs to tell it whether you're to
the left of the character to the right, and then which character
you're selecting. So obviously THINK C they implemented these things so that
you can get tabs in THINK C. But for a normal TextEdit control,
you don't have these. So the way that this all works is these
are Pascal functions that are like callbacks so TextEdit
calls these when it needs to, and then it
passes these registers, the data and the registers, and then
you have to copy them out into your own variables
and then do stuff and then return the
particular data in the particular registers
that it expects and then restore all the other registers that
you may have clobbered. So basically this
is all like you can just drop this in and then just call TETabEnable
on any TextEdit control and you get tabs for free,
and you can adjust the tab with by-- this is like a global that you
can just change that. I'm using four because any
code that you're managing in Amend was probably written in THINK C
and they default to tabs of four spaces.
So anyway, so that was a big chunk
of time that I spent trying to get that to
work properly. And actually--
the way that there is no documentation on how that actually like
how to use it, like in the THINK Reference it tells
you that you can call that you can use those
hooks, and it says which hook
you need. And then it just says, like, the data in the integer or the
data in each register entry and what you need to pass
back on exit.
But I didn't really know what to do with that information. And trying
to find any example code on the Internet that did that but it
was almost impossible. And I
found one Usenet post from 1988 where
somebody said there was an article in MacTutor magazine,
obviously before 1988 that had some code on how to
handle tabs in a TextEdit control.
So I was like, okay, well, that's helpful. So I went and tracked
down a CDROM archive of every MacTutor
article that they like, a CDROM that they put out.
I found that on archive.org. I downloaded it but I couldn't read
the ISO file because it wasn't like ISO 9660 format.
It was in a Mac specific format
from System 7 or whatever. So I had to use
Basilisk-- the emulator to boot System...
I think it was Mac OS 8, and anyway, it could read
the CDROM. I copied all the files out of there to my laptop
and was able to grep through them. I finally found the one
code example showing how to do stuff with tabs,
and it turns out that it was before all of this hook stuff before
all of this came to System 6 with the styled TextEdit
controls. And they were hooking into the raw, like, overriding the
ROM functions, basically. So every TextEdit control that
was on the screen, whether it
was in your application or not, would go through this code.
And it was written in Pascal, and it's just a mess. So I was like,
okay, well, that's not helpful. So then I found,
I guess the second Usenet post in the history of
whatever group it was like, comp.programmer.mac
something-- I don't know-- where somebody
actually posted some code to use
with THINK C 4, I think. And it
was this person here, Ari Halberstadt,
in comp.sys.mac.programmer. He posted some code
that showed, like, basically this part that
I didn't really know how to do because I've never done an assembly
in the 68K. He basically shows this and
the exit code, and then
everything in between is up to you because
he wasn't even doing it for implementing tabs, he was doing it for some other
reason. So I wrote all the code that runs in the
middle there that figures out the spacing, and then it accounts for,
like, if you do space-space tab, it doesn't do space, and then
a four character tab, it does space-space, and then a two character tab.
So all of that is all of this code.
So that's kind of neat. And I guess that's it.
As far as the development of this thing,
I'll post the code and the binary to my Kludge
BBS that I'll link on
the page below. If you want to download this,
and I'd be interested to see if anyone is actually wanting to use
this for tracking development of their own programs on
Classic Macs instead of like copying them to a Linux machine
and using Git. So if
you are, let me know, I'm going to be doing some
more features, like handling binary files and
figuring out all that stuff. So I'll be posting code updates to
the BBS. What else?
I think in a previous video I was talking about the "Odd Address" errors,
and I couldn't figure out if it was saying like--
odd as in weird, like you ended up in a weird address,
and THINK C knew enough to say like--
you're in a very low address range, like you shouldn't be here running
your own code. So you probably screwed something up. And it turns out that no,
it's literally an odd address. You are trying to do something-- you're trying
to reference an address that is odd instead of even.
And on the 68k, the 68000
you cannot reference memory
at an odd address you could on later 68k processors like
the 68020 I think.
So under normal circumstances, that should never happen because your variables
that you declare and stuff are aligned on even addresses.
So basically, if you get that error and THINK C says odd address,
it's because you are dereferencing a [pointer] incorrectly. And I
kept running into that doing things with arrays where
I would try and offset an array at
a certain index. And if I had like
the parentheses wrong and I was dereferencing the array
plus the index and that index was odd,
it would try and dereference an odd address and give me the odd address error.
So I finally figured that out. And then I did some reading on it and
then read that the 68000 cannot
deal with trying
to access memory at an odd address, but later 68k
processors can. So it's not an issue there anyway.
And then the other thing was that the memmove function that comes with THINK C 5
does not support overlapping buffers,
and it took me a while to figure out why I was getting corrupted data.
So if you want to move, like in my case, I had a big chunk
of memory. I was trying to allocate memory on the end of it and then
use memmove to shift all that memory down to the bottom of
the full allocation. And because it has to move like
a byte at the beginning to in the middle and then move the
next one over. By the time you get to that other part,
that data has already been overwritten. So like memmove on
like OpenBSD or on pretty much any modern system, I think, it uses
a temporary buffer, and it does things in the
correct order so that you're never overwriting data
in those overlapping buffers. So you can memmove like
you can shift data down basically with memmove.
You can't with THINK C. So I had to rewrite my code to use
more memory because I wasn't able to do that nicely
shifting of stuff. Anyway, so that's
pretty much it. Again. I'll post this code and
the program on my BBS. I hope to be able to
use it in the development of my next
program, which I will be covering here and
then I'll be able to actually generate a diff and commit it. And instead
of just like using the diff tool and showing
a diff and then doing nothing with it. So anyway,
thanks for watching. And I guess I'll see
you in the next video. Bye.