USB Typewriter and Interrobang App   Leave a comment

USB TypewriterI’ve always had a soft spot for classic devices, and so Jack Zylkin’s USB Typewriter project was naturally intriguing. Jack has come up with a set of electronics that enables a typewriter to be used — nondestructively — as a USB keyboard. Sabine and I constructed one of these “wonders of obsolescence,” and then I wrote a little iPad app to take the best advantage of the device.

The Typewriter

We found a beautiful 1930’s Corona Silent typewriter by Smith-Corona on eBay. When it arrived, it was in lovely condition with the exception of a fracture in the striker bar for the ‘a’ key. Fortunately, another eBay seller was offering a set of striker bars for use in crafting. We were able to replace (with slight modification) the striker, and transfer the ‘a’ typeface to the new bar.

All that remained was to install the USB Typewriter kit according to Jack Zylkin’s instructions. In order to minimize the visual footprint of the kit — but at the cost of losing use of the hardware toggle buttons — we hid the main control chip within the typewriter chassis. The pictures below show the completed typewriter, including the electrode strip for the main typewriter keys and reed switches and magnets for shift, return (mapped to margin release), space, and backspace. With the case closed, the only hint of modification is the USB cable emerging from below the frame in the rear. After calibration, the device works perfectly.

The Interrobang App

The Interrobang app is designed to overcome a few of the challenges inherent in using a typewriter as a computer keyboard. Because there are keys on many typewriters that don’t normally appear on a keyboard (like fraction keys), a scheme for custom key substitutions is integrated. This can also be used to correct for the fact that symbols on typewriters are often in non-standard positions.

Further, some characters are typed on a typewriter by ‘overtyping’ two keys. For instance, an exclamation mark is typed by pressing the period key, then backspace, then the apostrophe key. Similarly, accented characters can be typed using a letter and an apostrophe. This is where the app gets its name: the underappreciated interrobang symbol can be typed by using a question mark plus an apostrophe.

The finishing touches include optionally disabling the return key (useful if the typewriter’s carriage return is mapped to that key) and a custom ‘undo’ scheme. Finally, because the numbers 1 and 0 are often absent from typewriters — the letters l or I and O being substituted — the app will optionally attempt to recognize numbers like l99O and replace them with 1990.

Interrobang is based on the open source (MIT License) iPad word processor Edhita. Much of my work was to simplify the app, removing folder structure, advertisement, HTML preview, and other features unnecessary in this application. Most of the heavy lifting for character replacement is done in the main text view delegate’s textView: shouldChangeTextInRange: method:

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
    unichar last_removed = just_backspaced_char;
    
    if( (text.length == 0) && (range.length == 1) )
    {
        if( [[NSCharacterSet whitespaceAndNewlineCharacterSet]
             characterIsMember:[textView.text characterAtIndex:(range.location-1)]] )
            [self registerUndo];

        just_backspaced_char = [[fileTextView.text substringWithRange:range] characterAtIndex:0];
    }
    else
    {
        just_backspaced_char = 0;
        if( registerUndoNext )
        {
            [self registerUndo];
            registerUndoNext = false;
        }
    }
    
    if( text.length == 1 )
    {
        unichar replacement_char = [text characterAtIndex:0];
        NSString *to_add = [appDelegate replacementForSingleChar:replacement_char];
        Boolean singleSubstitution = true;
        if( to_add == nil )
        {
            to_add = text;
            singleSubstitution = false;
        }
        replacement_char = [to_add characterAtIndex:0];
        
        if( (last_removed != 0) )
        {
            NSString *dblRepl = [appDelegate replacementForDoubleChar:replacement_char and:last_removed];
            if( dblRepl != nil ) {
                NSString *oldText = textView.text;
                textView.text = [oldText stringByReplacingCharactersInRange:range withString:text];
                textView.selectedRange = NSMakeRange(range.location + 1, 0);
                [self registerUndo];
                textView.text = [oldText stringByReplacingCharactersInRange:range withString:dblRepl];
                textView.selectedRange = NSMakeRange(range.location + 1, 0);
                return false;
            }
        }
        
        if( [settings boolForKey:@"numbersSwitch"] )
        {
            if( numberBoundries == nil )
            {
                numberBoundries = [[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy];
                [numberBoundries formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]];
                [numberBoundries formUnionWithCharacterSet:[NSCharacterSet symbolCharacterSet]];
                couldBeNumbers = [[NSCharacterSet decimalDigitCharacterSet] mutableCopy];
                [couldBeNumbers addCharactersInString:@"IlOo"];
            }
            
            if( [numberBoundries characterIsMember:replacement_char] )
            {
                NSInteger lastBound = [textView.text rangeOfCharacterFromSet:numberBoundries
                                                                  options:NSBackwardsSearch
                                                                    range:NSMakeRange(0, range.location)].location;
                if( lastBound == NSNotFound ) lastBound = -1;
                NSRange lastWordRange = NSMakeRange(lastBound + 1, range.location - lastBound - 1);
                NSMutableString *lastWord = [[textView.text substringWithRange:lastWordRange] mutableCopy];
                
                if([lastWord length] > 1 &&           // If the previous "word" has at least two characters
                   [lastWord rangeOfCharacterFromSet: // None  couldn't be numeric (0-9, l, I, o, O)
                    [couldBeNumbers invertedSet]].location == NSNotFound &&
                   [lastWord rangeOfCharacterFromSet: // And at least one is a legit decimal digit (0-9)
                    [NSCharacterSet decimalDigitCharacterSet]].location != NSNotFound )
                {
                    for( int i = 0; i < [lastWord length]; i++ )
                    {
                        unichar ch = [lastWord characterAtIndex:i];
                        if( ch == 'l' || ch == 'I' )
                            [lastWord replaceCharactersInRange:NSMakeRange(i, 1) withString:@"1"];
                        if( ch == 'O' || ch == 'o' )
                            [lastWord replaceCharactersInRange:NSMakeRange(i, 1) withString:@"0"];
                    }
                    
                    [self registerUndo];
                    NSRange oldSelection = textView.selectedRange;
                    textView.text = [textView.text stringByReplacingCharactersInRange:lastWordRange withString:lastWord];
                    textView.selectedRange = oldSelection;
                }
                
                [lastWord release];
            }
        }
        
        if( [[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:replacement_char] )
            registerUndoNext = true;
        
        if( replacement_char == '\n' && ![settings boolForKey:@"returnSwitch"] ) {
            textView.text = [textView.text stringByReplacingCharactersInRange:range withString:@" "];
            textView.selectedRange = NSMakeRange(range.location + 1, 0);
            return false;
        }
        
        if( replacement_char == '\n' && [settings boolForKey:@"doubleNLSwitch"] ) {
            textView.text = [textView.text stringByReplacingCharactersInRange:range withString:@"\n\n"];
            textView.selectedRange = NSMakeRange(range.location + 2, 0);
            return false;
        }
        
        if( singleSubstitution )
        {
            textView.text = [textView.text stringByReplacingCharactersInRange:range withString:to_add];
            textView.selectedRange = NSMakeRange(range.location + 1, 0);
            return false;
        }
    }
    
    if( text.length > 1 || range.length > 1 ) [self registerUndo];
    
    return true;
}

It all seems to work well. Screenshots are below:

Posted 5 Jul 2014 by John McManigle in Technical

Leave a Reply

Your email address will not be published. Required fields are marked *