diff --git a/Frameworks/RSParser/Feeds/FeedParser.swift b/Frameworks/RSParser/Feeds/FeedParser.swift index 083478074..7a2b0e126 100644 --- a/Frameworks/RSParser/Feeds/FeedParser.swift +++ b/Frameworks/RSParser/Feeds/FeedParser.swift @@ -8,54 +8,14 @@ import Foundation -// FeedParser handles the various syndication feed types. -// It might be a good idea to do a plugin-style architecture here instead — -// but feed formats don’t appear all that often, so it’s probably not necessary. +// FeedParser handles RSS, Atom, JSON Feed, and RSS-in-JSON. +// You don’t need to know the type of feed. public struct FeedParser { - static let minNumberOfBytesRequired = 128 + public static func parse(_ parserData: ParserData) throws -> ParsedFeed? { - public static func feedType(_ parserData: ParserData) -> FeedType { - - // Can call with partial data — while still downloading, for instance. - // If there’s not enough data, return .unknown. Ask again when there’s more data. - // If it’s definitely not a feed, return .notAFeed. - // - // This should be fast enough to call on the main thread. - - if parserData.data.count < minNumberOfBytesRequired { - return .unknown - } - - let nsdata = parserData.data as NSData - if nsdata.isProbablyJSONFeed() { - return .jsonFeed - } - if nsdata.isProbablyRSSInJSON() { - return .rssInJSON - } - - if nsdata.isProbablyHTML() { - return .notAFeed - } - - if nsdata.isProbablyRSS() { - return .rss - } - if nsdata.isProbablyAtom() { - return .atom - } - - return .notAFeed - } - - public static func parseFeed(_ parserData: ParserData) throws -> ParsedFeed? { - - // All the concrete parsers return a ParsedFeed struct. - // Related: ParsedItem, ParsedAuthor, ParsedHub, ParsedAttachment. - // - // This is probably fast enough to call on the main thread — + // This is generally fast enough to call on the main thread — // but it’s probably a good idea to use a background queue if // you might be doing a lot of parsing. (Such as in a feed reader.) diff --git a/Frameworks/RSParser/Feeds/FeedType.swift b/Frameworks/RSParser/Feeds/FeedType.swift index 498284327..6cdbbf358 100644 --- a/Frameworks/RSParser/Feeds/FeedType.swift +++ b/Frameworks/RSParser/Feeds/FeedType.swift @@ -16,3 +16,40 @@ public enum FeedType { case unknown case notAFeed } + + +private let minNumberOfBytesRequired = 128 + +public func feedType(_ parserData: ParserData) -> FeedType { + + // Can call with partial data — while still downloading, for instance. + // If there’s not enough data, return .unknown. Ask again when there’s more data. + // If it’s definitely not a feed, return .notAFeed. + // + // This is fast enough to call on the main thread. + + if parserData.data.count < minNumberOfBytesRequired { + return .unknown + } + + let nsdata = parserData.data as NSData + if nsdata.isProbablyJSONFeed() { + return .jsonFeed + } + if nsdata.isProbablyRSSInJSON() { + return .rssInJSON + } + + if nsdata.isProbablyHTML() { + return .notAFeed + } + + if nsdata.isProbablyRSS() { + return .rss + } + if nsdata.isProbablyAtom() { + return .atom + } + + return .notAFeed +} diff --git a/Frameworks/RSParser/README.md b/Frameworks/RSParser/README.md index a8262b380..665dffc47 100644 --- a/Frameworks/RSParser/README.md +++ b/Frameworks/RSParser/README.md @@ -65,3 +65,11 @@ Normally I avoid this kind of thing *strenuously*. I prefer to work at the highe But my more-than-a-decade of experience parsing XML has led me to this solution, which — last time I checked, which was, admittedly, a few years ago — was not only fastest but also uses the least memory. (The two things are related, of course: creating objects is bad for performance, so this code attempts to do the minimum possible.) All that low-level stuff is encapsulated, however. If you just want to parse one of the popular feed formats, see `FeedParser`, which makes it easy and Swift-y. + +## Thread safety + +Everything here is thread-safe. + +Everything’s pretty fast, too, so you probably could just use the main thread/queue. But it’s totally a-okay to use a non-serial background queue. + + diff --git a/Frameworks/RSParser/RSParser.xcodeproj/project.pbxproj b/Frameworks/RSParser/RSParser.xcodeproj/project.pbxproj index 396ecb1a0..98c3cf81f 100644 --- a/Frameworks/RSParser/RSParser.xcodeproj/project.pbxproj +++ b/Frameworks/RSParser/RSParser.xcodeproj/project.pbxproj @@ -71,6 +71,8 @@ 849A03E21F00902C00122600 /* RSDateParserTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 849A03E11F00902C00122600 /* RSDateParserTests.m */; }; 849A03E41F00912100122600 /* HTMLMetadataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A03E31F00912100122600 /* HTMLMetadataTests.swift */; }; 849A03E61F00952400122600 /* HTMLLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A03E51F00952400122600 /* HTMLLinkTests.swift */; }; + 849A03E81F01F88600122600 /* ScriptingNews.json in Resources */ = {isa = PBXBuildFile; fileRef = 849A03E71F01F88600122600 /* ScriptingNews.json */; }; + 849A03EA1F01F92B00122600 /* inessential.json in Resources */ = {isa = PBXBuildFile; fileRef = 849A03E91F01F92B00122600 /* inessential.json */; }; 84D81BDC1EFA28E700652332 /* RSParser.h in Headers */ = {isa = PBXBuildFile; fileRef = 84D81BDA1EFA28E700652332 /* RSParser.h */; settings = {ATTRIBUTES = (Public, ); }; }; 84D81BDE1EFA2B7D00652332 /* ParsedFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D81BDD1EFA2B7D00652332 /* ParsedFeed.swift */; }; 84D81BE01EFA2BAE00652332 /* FeedType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D81BDF1EFA2BAE00652332 /* FeedType.swift */; }; @@ -157,6 +159,8 @@ 849A03E11F00902C00122600 /* RSDateParserTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RSDateParserTests.m; sourceTree = ""; }; 849A03E31F00912100122600 /* HTMLMetadataTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLMetadataTests.swift; sourceTree = ""; }; 849A03E51F00952400122600 /* HTMLLinkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLLinkTests.swift; sourceTree = ""; }; + 849A03E71F01F88600122600 /* ScriptingNews.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ScriptingNews.json; sourceTree = ""; }; + 849A03E91F01F92B00122600 /* inessential.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = inessential.json; sourceTree = ""; }; 84D81BD91EFA28E700652332 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 84D81BDA1EFA28E700652332 /* RSParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RSParser.h; sourceTree = ""; }; 84D81BDD1EFA2B7D00652332 /* ParsedFeed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ParsedFeed.swift; path = Feeds/ParsedFeed.swift; sourceTree = ""; }; @@ -317,9 +321,11 @@ 849A03C71F0081EA00122600 /* EMarley.rss */, 849A03C81F0081EA00122600 /* furbo.html */, 849A03C91F0081EA00122600 /* inessential.html */, + 849A03E91F01F92B00122600 /* inessential.json */, 849A03CA1F0081EA00122600 /* KatieFloyd.rss */, 849A03CB1F0081EA00122600 /* manton.rss */, 849A03CC1F0081EA00122600 /* OneFootTsunami.atom */, + 849A03E71F01F88600122600 /* ScriptingNews.json */, 849A03CD1F0081EA00122600 /* scriptingNews.rss */, 849A03CE1F0081EA00122600 /* sixcolors.html */, 849A03CF1F0081EA00122600 /* Subs.opml */, @@ -495,9 +501,11 @@ 849A03D61F0081EA00122600 /* manton.rss in Resources */, 849A03D11F0081EA00122600 /* DaringFireball.rss in Resources */, 849A03D01F0081EA00122600 /* DaringFireball.html in Resources */, + 849A03EA1F01F92B00122600 /* inessential.json in Resources */, 849A03D71F0081EA00122600 /* OneFootTsunami.atom in Resources */, 849A03D41F0081EA00122600 /* inessential.html in Resources */, 849A03D31F0081EA00122600 /* furbo.html in Resources */, + 849A03E81F01F88600122600 /* ScriptingNews.json in Resources */, 849A03D91F0081EA00122600 /* sixcolors.html in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Frameworks/RSParser/RSParserTests/FeedParserTypeTests.swift b/Frameworks/RSParser/RSParserTests/FeedParserTypeTests.swift index 04fd8089d..b08e8781a 100644 --- a/Frameworks/RSParser/RSParserTests/FeedParserTypeTests.swift +++ b/Frameworks/RSParser/RSParserTests/FeedParserTypeTests.swift @@ -16,28 +16,28 @@ class FeedParserTypeTests: XCTestCase { func testDaringFireballHTMLType() { let d = parserData("DaringFireball", "html", "http://daringfireball.net/") - let type = FeedParser.feedType(d) + let type = feedType(d) XCTAssertTrue(type == .notAFeed) } func testFurboHTMLType() { let d = parserData("furbo", "html", "http://furbo.org/") - let type = FeedParser.feedType(d) + let type = feedType(d) XCTAssertTrue(type == .notAFeed) } func testInessentialHTMLType() { let d = parserData("inessential", "html", "http://inessential.com/") - let type = FeedParser.feedType(d) + let type = feedType(d) XCTAssertTrue(type == .notAFeed) } func testSixColorsHTMLType() { let d = parserData("sixcolors", "html", "https://sixcolors.com/") - let type = FeedParser.feedType(d) + let type = feedType(d) XCTAssertTrue(type == .notAFeed) } @@ -46,28 +46,28 @@ class FeedParserTypeTests: XCTestCase { func testEMarleyRSSType() { let d = parserData("EMarley", "rss", "https://medium.com/@emarley") - let type = FeedParser.feedType(d) + let type = feedType(d) XCTAssertTrue(type == .rss) } func testScriptingNewsRSSType() { let d = parserData("scriptingNews", "rss", "http://scripting.com/") - let type = FeedParser.feedType(d) + let type = feedType(d) XCTAssertTrue(type == .rss) } func testKatieFloydRSSType() { let d = parserData("KatieFloyd", "rss", "https://katiefloyd.com/") - let type = FeedParser.feedType(d) + let type = feedType(d) XCTAssertTrue(type == .rss) } func testMantonRSSType() { let d = parserData("manton", "rss", "http://manton.org/") - let type = FeedParser.feedType(d) + let type = feedType(d) XCTAssertTrue(type == .rss) } @@ -77,28 +77,77 @@ class FeedParserTypeTests: XCTestCase { // File extension is .rss, but it’s really an Atom feed. let d = parserData("DaringFireball", "rss", "http://daringfireball.net/") - let type = FeedParser.feedType(d) + let type = feedType(d) XCTAssertTrue(type == .atom) } func testOneFootTsunamiAtomType() { let d = parserData("OneFootTsunami", "atom", "http://onefoottsunami.com/") - let type = FeedParser.feedType(d) + let type = feedType(d) XCTAssertTrue(type == .atom) } + // MARK: RSS-in-JSON + + func testScriptingNewsJSONType() { + + let d = parserData("ScriptingNews", "json", "http://scripting.com/") + let type = feedType(d) + XCTAssertTrue(type == .rssInJSON) + } + + // MARK: JSON Feed + + func testInessentialJSONFeedType() { + + let d = parserData("inessential", "json", "http://inessential.com/") + let type = feedType(d) + XCTAssertTrue(type == .jsonFeed) + } + // MARK: Performance func testFeedTypePerformance() { - // I get 0.000079 on my 2012 iMac. feedType is fast, at least in this case. + // 0.000 on my 2012 iMac. let d = parserData("EMarley", "rss", "https://medium.com/@emarley") self.measure { - let _ = FeedParser.feedType(d) + let _ = feedType(d) } } + + func testFeedTypePerformance2() { + + // 0.000 on my 2012 iMac. + + let d = parserData("inessential", "json", "http://inessential.com/") + self.measure { + let _ = feedType(d) + } + } + + func testFeedTypePerformance3() { + + // 0.000 on my 2012 iMac. + + let d = parserData("DaringFireball", "html", "http://daringfireball.net/") + self.measure { + let _ = feedType(d) + } + } + + func testFeedTypePerformance4() { + + // 0.001 on my 2012 iMac. + + let d = parserData("DaringFireball", "rss", "http://daringfireball.net/") + self.measure { + let _ = feedType(d) + } + } + } func parserData(_ filename: String, _ fileExtension: String, _ url: String) -> ParserData { diff --git a/Frameworks/RSParser/RSParserTests/Resources/ScriptingNews.json b/Frameworks/RSParser/RSParserTests/Resources/ScriptingNews.json new file mode 100644 index 000000000..2d658e00f --- /dev/null +++ b/Frameworks/RSParser/RSParserTests/Resources/ScriptingNews.json @@ -0,0 +1,945 @@ +{ + "rss": { + "version": "2.0", + "xmlns:source": "http://source.scripting.com/", + "channel": { + "title": "Scripting News", + "link": "http://scripting.com/", + "description": "Scripting News, the weblog started in 1997 that bootstrapped the blogging revolution.", + "pubDate": "Mon, 26 Jun 2017 19:40:58 GMT", + "lastBuildDate": "Mon, 26 Jun 2017 19:41:48 GMT", + "language": "en-us", + "copyright": "© 1994-2017 Dave Winer.", + "generator": "oldSchool v0.42c", + "docs": "https://github.com/scripting/Scripting-News/blob/master/rss-in-json/README.md", + "source:localTime": "Mon, June 26, 2017 3:41 PM EDT", + "cloud": { + "domain": "rpc.rsscloud.io", + "port": 5337, + "path": "/pleaseNotify", + "registerProcedure": "", + "protocol": "http-post" + }, + "source:account": [ + { + "service": "twitter", + "#value": "davewiner" + }, + { + "service": "facebook", + "#value": "dave.winer.12" + }, + { + "service": "github", + "#value": "scripting" + }, + { + "service": "linkedin", + "#value": "scripting" + } + ], + "item": [ + { + "link": "http://scripting.com/2017/06/26.html#a080605", + "description": "Good morning students and teachers! 🍏 ", + "pubDate": "Mon, 26 Jun 2017 12:20:05 GMT", + "guid": "http://scripting.com/2017/06/26.html#a080605", + "source:outline": { + "text": "Good morning students and teachers! :green_apple: ", + "created": "Mon, 26 Jun 2017 12:20:05 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/26.html#a080605" + } + }, + { + "link": "http://scripting.com/2017/06/26.html#a030658", + "description": "This is the human side of Health care is socialist. ", + "pubDate": "Mon, 26 Jun 2017 19:40:58 GMT", + "guid": "http://scripting.com/2017/06/26.html#a030658", + "source:outline": { + "text": "This is the human side of Health care is socialist. ", + "created": "Mon, 26 Jun 2017 19:40:58 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/26.html#a030658" + } + }, + { + "link": "http://scripting.com/2017/06/26.html#a020604", + "description": "Interesting Politico piece posits that Trump acts as if he's mayor of the United States. If NYC is his model, that mayor is esp powerless, because the governor of the state also has a lot of power over the city. It's approx 1/2 of the population of the state, and probably much more than 1/2 of the money. For example, the MTA, which runs the buses and subway, is run by the state, not the city.", + "pubDate": "Mon, 26 Jun 2017 18:10:04 GMT", + "guid": "http://scripting.com/2017/06/26.html#a020604", + "source:outline": { + "text": "Interesting Politico piece posits that Trump acts as if he's mayor of the United States. If NYC is his model, that mayor is esp powerless, because the governor of the state also has a lot of power over the city. It's approx 1/2 of the population of the state, and probably much more than 1/2 of the money. For example, the MTA, which runs the buses and subway, is run by the state, not the city.", + "created": "Mon, 26 Jun 2017 18:10:04 GMT", + "type": "outline", + "image": "http://scripting.com/images/2017/06/26/quimby.png", + "permalink": "http://scripting.com/2017/06/26.html#a020604" + } + }, + { + "link": "http://scripting.com/2017/06/26.html#a020602", + "description": "Brent asks if the length in enclosures in RSS-in-JSON is a number or string. That's what the test podcast below is for. ", + "pubDate": "Mon, 26 Jun 2017 18:05:02 GMT", + "guid": "http://scripting.com/2017/06/26.html#a020602", + "source:outline": { + "text": "Brent asks if the length in enclosures in \"RSS-in-JSON\" is a number or string. That's what the test podcast below is for. ", + "created": "Mon, 26 Jun 2017 18:05:02 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/26.html#a020602" + } + }, + { + "link": "http://scripting.com/2017/06/26.html#a010633", + "description": "From time to time I have to do a podcast to test things out. This is one of those times. Let's see what happens. ", + "pubDate": "Mon, 26 Jun 2017 17:29:33 GMT", + "guid": "http://scripting.com/2017/06/26.html#a010633", + "enclosure": { + "url": "http://scripting.com/2017/06/26/yetAnotherTestPodcast.m4a", + "type": "audio/mpeg", + "length": 277413 + }, + "source:outline": { + "text": "From time to time I have to do a podcast to test things out. This is one of those times. Let's see what happens. ", + "created": "Mon, 26 Jun 2017 17:29:33 GMT", + "type": "outline", + "enclosure": "http://scripting.com/2017/06/26/yetAnotherTestPodcast.m4a", + "enclosureType": "audio/mpeg", + "enclosureLength": "277413", + "permalink": "http://scripting.com/2017/06/26.html#a010633" + } + }, + { + "link": "http://scripting.com/2017/06/26.html#a110603", + "description": "Body shaming is wrong no matter who you're using as the example. Someone is being hurt by this. No, I don't care how much you have suffered.", + "pubDate": "Mon, 26 Jun 2017 15:26:03 GMT", + "guid": "http://scripting.com/2017/06/26.html#a110603", + "source:outline": { + "text": "Body shaming is wrong no matter who you're using as the example. Someone is being hurt by this. No, I don't care how much you have suffered.", + "created": "Mon, 26 Jun 2017 15:26:03 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/26.html#a110603" + } + }, + { + "link": "http://scripting.com/2017/06/26.html#a090616", + "description": "I need an app to view RSS feeds in the browser because Chrome and Safari refuse to let me do that. I'd love to hear the reason why. ", + "pubDate": "Mon, 26 Jun 2017 13:24:16 GMT", + "guid": "http://scripting.com/2017/06/26.html#a090616", + "source:outline": { + "text": "I need an app to view RSS feeds in the browser because Chrome and Safari refuse to let me do that. I'd love to hear the reason why. ", + "created": "Mon, 26 Jun 2017 13:24:16 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/26.html#a090616" + } + }, + { + "title": "Subscribable feed lists give power to users", + "link": "http://scripting.com/2017/06/26.html#a080636", + "description": "

An interesting comment from Chris Aldrich about subscribing to lists of feeds in a thread on the Woodwind app site on GitHub.

\n

Here's the basic idea. There's a difference between importing OPML into a reader and subscribing to it. The latter is very powerful, for the user, but a lot of RSS reader devs may not want their users to have that much power. It's not a very hard feature to implement.

\n

The idea has been much-discussed here. We call them reading lists. Michael Arrington even wrote a TechCrunch piece about it in 2005.

\n

Subscribable OPML is something all my readers have been able to do through an OPML feature called inclusion. I wrote a howto for a River5 user re inclusion just last week.

\n

Share Your OPML was a service I operated for a while. It made it possible to manage your OPML separate from the reader you used. It was meant to encourage readers to support subscribable OPML. I'm looking for an excuse to bring it back, but first we need a base of shared feed lists.

\n

A lot of good stuff can be done if feed readers are willing to delegate list management to other services. IMHO the only reason a reader developer wouldn't do it is because they want to lock users in. If I let you edit your feed list elsewhere that means you could give the list to another vendor and have a choice which to use. It's really something users should demand, esp if you're paying for the service.

\n", + "pubDate": "Mon, 26 Jun 2017 12:24:36 GMT", + "guid": "http://scripting.com/2017/06/26.html#a080636", + "source:outline": { + "text": "Subscribable feed lists give power to users", + "created": "Mon, 26 Jun 2017 12:24:36 GMT", + "type": "outline", + "subs": [ + { + "text": "An interesting comment from Chris Aldrich about subscribing to lists of feeds in a thread on the Woodwind app site on GitHub. ", + "created": "Mon, 26 Jun 2017 12:24:49 GMT", + "image": "http://static.scripting.com/larryKing/images/2014/05/25/goodHumor.gif", + "permalink": "http://scripting.com/2017/06/26.html#a080649" + }, + { + "text": "Here's the basic idea. There's a difference between importing OPML into a reader and subscribing to it. The latter is very powerful, for the user, but a lot of RSS reader devs may not want their users to have that much power. It's not a very hard feature to implement. ", + "created": "Mon, 26 Jun 2017 12:40:32 GMT", + "permalink": "http://scripting.com/2017/06/26.html#a080632" + }, + { + "text": "The idea has been much-discussed here. We call them reading lists. Michael Arrington even wrote a TechCrunch piece about it in 2005. ", + "created": "Mon, 26 Jun 2017 12:26:01 GMT", + "permalink": "http://scripting.com/2017/06/26.html#a080601" + }, + { + "text": "Subscribable OPML is something all my readers have been able to do through an OPML feature called inclusion. I wrote a howto for a \"River5\" user re inclusion just last week.", + "created": "Mon, 26 Jun 2017 12:27:15 GMT", + "permalink": "http://scripting.com/2017/06/26.html#a080615" + }, + { + "text": "Share Your OPML was a service I operated for a while. It made it possible to manage your OPML separate from the reader you used. It was meant to encourage readers to support subscribable OPML. I'm looking for an excuse to bring it back, but first we need a base of shared feed lists. ", + "created": "Mon, 26 Jun 2017 12:33:10 GMT", + "permalink": "http://scripting.com/2017/06/26.html#a080610" + }, + { + "text": "A lot of good stuff can be done if feed readers are willing to delegate list management to other services. IMHO the only reason a reader developer wouldn't do it is because they want to lock users in. If I let you edit your feed list elsewhere that means you could give the list to another vendor and have a choice which to use. It's really something users should demand, esp if you're paying for the service. ", + "created": "Mon, 26 Jun 2017 12:47:13 GMT", + "permalink": "http://scripting.com/2017/06/26.html#a080613" + } + ], + "permalink": "http://scripting.com/2017/06/26.html#a080636" + } + }, + { + "link": "http://scripting.com/2017/06/25.html#a080631", + "description": "Good morning Internet guzzlers! 🍺", + "pubDate": "Sun, 25 Jun 2017 12:27:31 GMT", + "guid": "http://scripting.com/2017/06/25.html#a080631", + "source:outline": { + "text": "Good morning Internet guzzlers! :beer:", + "created": "Sun, 25 Jun 2017 12:27:31 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/25.html#a080631" + } + }, + { + "link": "http://scripting.com/2017/06/25.html#a080606", + "description": "So glad I stopped worrying about Facebook and am now blogging Old School style on scripting.com. I've found my sea legs once again. ", + "pubDate": "Sun, 25 Jun 2017 12:43:06 GMT", + "guid": "http://scripting.com/2017/06/25.html#a080606", + "source:outline": { + "text": "So glad I stopped worrying about Facebook and am now blogging Old School style on scripting.com. I've found my sea legs once again. ", + "created": "Sun, 25 Jun 2017 12:43:06 GMT", + "type": "tweet", + "tweetId": "878956761302147072", + "tweetUserName": "davewiner", + "permalink": "http://scripting.com/2017/06/25.html#a080606" + } + }, + { + "link": "http://scripting.com/2017/06/25.html#a080623", + "description": "Dan Shafer died. I knew Dan from the Mac developer community in the 80s, hired him to write the first docs for Frontier. Bon voyage mi amigo! 💥", + "pubDate": "Sun, 25 Jun 2017 12:56:23 GMT", + "guid": "http://scripting.com/2017/06/25.html#a080623", + "source:outline": { + "text": "Dan Shafer died. I knew Dan from the Mac developer community in the 80s, hired him to write the first docs for \"Frontier\". Bon voyage mi amigo! :boom:", + "created": "Sun, 25 Jun 2017 12:56:23 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/25.html#a080623" + } + }, + { + "link": "http://scripting.com/2017/06/25.html#a080624", + "description": "Money was a big issue yesterday in the nascent tech blogosphere. First, you do this because you love it, not because it pays well. (It doesn't pay at all.) Now I'd like to take you back to a discsussion that was had many years ago that resulted in this conclusion. You don't make money from this work, but it leads to opportunities where you can make money. Ideas and information make their way to you and if you are so-inclined you can make money by investing in those ideas. No sure things, but some bloggers have made billions, and others have made millions. And others have made a decent living. Not from their blog but because they blog. ", + "pubDate": "Sun, 25 Jun 2017 12:29:24 GMT", + "guid": "http://scripting.com/2017/06/25.html#a080624", + "source:outline": { + "text": "Money was a big issue yesterday in the nascent tech blogosphere. First, you do this because you love it, not because it pays well. (It doesn't pay at all.) Now I'd like to take you back to a discsussion that was had many years ago that resulted in this conclusion. You don't make money from this work, but it leads to opportunities where you can make money. Ideas and information make their way to you and if you are so-inclined you can make money by investing in those ideas. No sure things, but some bloggers have made billions, and others have made millions. And others have made a decent living. Not from their blog but because they blog. ", + "created": "Sun, 25 Jun 2017 12:29:24 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/25.html#a080624" + } + }, + { + "link": "http://scripting.com/2017/06/25.html#a080631", + "description": "In 2015 I wrote that Dropbox could be the king of the one-page app. Because storage is the thing the web doesn't, on its own, do, and storage is the thing Dropbox does best. And they have an API, and they understood the connection to one-page-apps earlier than anyone. But it didn't happen. I've emailed with people at Dropbox from time to time and the best explanation I can come up with is that they are focused in different areas. It seems to me, from my outside perch, that they are trying to become a competitor to Google's and Microsoft's Office products. I was hoping they'd become a platform, focusing on distribution and investment in startups.", + "pubDate": "Sun, 25 Jun 2017 12:32:31 GMT", + "guid": "http://scripting.com/2017/06/25.html#a080631", + "source:outline": { + "text": "In 2015 I wrote that Dropbox could be the king of the one-page app. Because storage is the thing the web doesn't, on its own, do, and storage is the thing Dropbox does best. And they have an API, and they understood the connection to one-page-apps earlier than anyone. But it didn't happen. I've emailed with people at Dropbox from time to time and the best explanation I can come up with is that they are focused in different areas. It seems to me, from my outside perch, that they are trying to become a competitor to Google's and Microsoft's Office products. I was hoping they'd become a platform, focusing on distribution and investment in startups.", + "created": "Sun, 25 Jun 2017 12:32:31 GMT", + "type": "outline", + "image": "http://radio3.io/icons/clarus.gif", + "permalink": "http://scripting.com/2017/06/25.html#a080631" + } + }, + { + "title": "Code mode is for real", + "link": "http://scripting.com/2017/06/25.html#a110613", + "description": "

\"Code

\n", + "pubDate": "Sun, 25 Jun 2017 15:53:13 GMT", + "guid": "http://scripting.com/2017/06/25.html#a110613", + "source:outline": { + "text": "Code mode is for real", + "created": "Sun, 25 Jun 2017 15:53:13 GMT", + "type": "outline", + "subs": [ + { + "text": "\"Code", + "created": "Sun, 25 Jun 2017 15:53:18 GMT", + "permalink": "http://scripting.com/2017/06/25.html#a110618" + } + ], + "permalink": "http://scripting.com/2017/06/25.html#a110613" + } + }, + { + "link": "http://scripting.com/2017/06/24.html#a100601", + "description": "Good morning sports fans! 🏈", + "pubDate": "Sat, 24 Jun 2017 14:57:01 GMT", + "guid": "http://scripting.com/2017/06/24.html#a100601", + "source:outline": { + "text": "Good morning sports fans! :football:", + "created": "Sat, 24 Jun 2017 14:57:01 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/24.html#a100601" + } + }, + { + "link": "http://scripting.com/2017/06/24.html#a040606", + "description": "Health care is socialist is getting a bunch of new reads today thanks to some powerful RTs.", + "pubDate": "Sat, 24 Jun 2017 20:50:06 GMT", + "guid": "http://scripting.com/2017/06/24.html#a040606", + "source:outline": { + "text": "Health care is socialist is getting a bunch of new reads today thanks to some powerful RTs.", + "created": "Sat, 24 Jun 2017 20:50:06 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/24.html#a040606" + } + }, + { + "link": "http://scripting.com/2017/06/24.html#a020644", + "description": "Today is emoji day ⭐️ on Scripting News. We have all the ⭐️ best emoji. And they're free, for you, the best ⭐️ people in the universe, the readers of ⭐️ this humble blog. ⭐️ ", + "pubDate": "Sat, 24 Jun 2017 18:34:44 GMT", + "guid": "http://scripting.com/2017/06/24.html#a020644", + "source:outline": { + "text": "Today is emoji day :star: on Scripting News. We have all the :star: best emoji. And they're free, for you, the best :star: people in the universe, the readers of :star: this humble blog. :star: ", + "created": "Sat, 24 Jun 2017 18:34:44 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/24.html#a020644" + } + }, + { + "link": "http://scripting.com/2017/06/24.html#a020620", + "description": "It makes sense that because of reconcilliation Repubs have a hard time repealing ObamaCare. It seems fair that you should have to have 60 votes to repeal something that required 60 votes to pass. How will they explain it to the voters they've been lying to about \"repeal and replace.\" Double-talk. Swamp-talk. 👍", + "pubDate": "Sat, 24 Jun 2017 18:20:20 GMT", + "guid": "http://scripting.com/2017/06/24.html#a020620", + "source:outline": { + "text": "It makes sense that because of reconcilliation Repubs have a hard time repealing ObamaCare. It seems fair that you should have to have 60 votes to repeal something that required 60 votes to pass. How will they explain it to the voters they've been lying to about \"repeal and replace.\" Double-talk. Swamp-talk. :+1:", + "created": "Sat, 24 Jun 2017 18:20:20 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/24.html#a020620" + } + }, + { + "link": "http://scripting.com/2017/06/24.html#a020649", + "description": "I've started a chatroom on Gitter. Not sure what I'll use it for. Also not sure if it's open for anyone to join. I want it to be. The community guidelines apply. Keep it short, respectful and on-topic, and no spam. ⚾️", + "pubDate": "Sat, 24 Jun 2017 18:05:49 GMT", + "guid": "http://scripting.com/2017/06/24.html#a020649", + "source:outline": { + "text": "I've started a chatroom on Gitter. Not sure what I'll use it for. Also not sure if it's open for anyone to join. I want it to be. The community guidelines apply. Keep it short, respectful and on-topic, and no spam. :baseball:", + "created": "Sat, 24 Jun 2017 18:05:49 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/24.html#a020649" + } + }, + { + "link": "http://scripting.com/2017/06/24.html#a110617", + "description": "BTW, don't ask me to pitch RSS, I won't do it, because the request is based on a misunderstanding. RSS is not a product, it's a format. I have never made a dime from it. You have as much at stake in its success as I do. So I always turn it around and ask the person who asked me for a pitch to instead pitch me on it. I don't budge on this. Ever. 🏀", + "pubDate": "Sat, 24 Jun 2017 15:19:17 GMT", + "guid": "http://scripting.com/2017/06/24.html#a110617", + "source:outline": { + "text": "BTW, don't ask me to pitch RSS, I won't do it, because the request is based on a misunderstanding. RSS is not a product, it's a format. I have never made a dime from it. You have as much at stake in its success as I do. So I always turn it around and ask the person who asked me for a pitch to instead pitch me on it. I don't budge on this. Ever. :basketball:", + "created": "Sat, 24 Jun 2017 15:19:17 GMT", + "type": "outline", + "image": "http://scripting.com/images/2017/06/24/rssTShirt.png", + "permalink": "http://scripting.com/2017/06/24.html#a110617" + } + }, + { + "link": "http://scripting.com/2017/06/24.html#a110610", + "description": "Is anyone using the JSON version of the Scripting News feed in their feed reader? It's a bit of a trick question, in a way, because as far as I know, only River5 and Electric River support this format. If so, send me an email at my address, on the About page here. 🍰", + "pubDate": "Sat, 24 Jun 2017 15:01:10 GMT", + "guid": "http://scripting.com/2017/06/24.html#a110610", + "source:outline": { + "text": "Is anyone using the JSON version of the Scripting News feed in their feed reader? It's a bit of a trick question, in a way, because as far as I know, only \"River5\" and \"Electric River\" support this format. If so, send me an email at my address, on the About page here. :cake:", + "created": "Sat, 24 Jun 2017 15:01:10 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/24.html#a110610" + } + }, + { + "link": "http://scripting.com/2017/06/24.html#a010650", + "description": "Journalists keep making a serious technology error -- assuming the only damage Russia can do to our government is during elections. 🇺🇸 ", + "pubDate": "Sat, 24 Jun 2017 05:50:50 GMT", + "guid": "http://scripting.com/2017/06/24.html#a010650", + "source:outline": { + "text": "Journalists keep making a serious technology error -- assuming the only damage Russia can do to our government is during elections. :us: ", + "created": "Sat, 24 Jun 2017 05:50:50 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/24.html#a010650" + } + }, + { + "link": "http://scripting.com/2017/06/24.html#a100632", + "description": "I get my health insurance through the ACA. It's very much alive, Spicer. Without it I would not have health insurance. 🍋 ", + "pubDate": "Sat, 24 Jun 2017 14:26:32 GMT", + "guid": "http://scripting.com/2017/06/24.html#a100632", + "source:outline": { + "text": "I get my health insurance through the ACA. It's very much alive, Spicer. Without it I would not have health insurance. :lemon: ", + "created": "Sat, 24 Jun 2017 14:26:32 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/24.html#a100632" + } + }, + { + "title": "The sad state of tech news in 2017", + "link": "http://scripting.com/2017/06/24.html#a090648", + "description": "

It won't take much to reboot the tech blogosphere, just a few bloggers with ideas who listen to each other and want to work with each other. That was the idea behind blogrolls, to visibly show the relationships.

\n
    \n\t
  • I'm still thinking about how to integrate a blogroll with the new design of Scripting News. I pushed everything aside to get a clean look for the new site. I want to avoid bringing it all back.
  • \n\t
\n

So, we have a few people who are writing and listening. That's a needed first step. Next we need a way to announce and hear about new tech products. Not just ones that get VC backing or come from big companies. We already hear about those products through TechMeme and the pubs that contribute to it. We also have platform-specific news about tech products, it's more limited, but it's there.

\n

It will likely start with word of mouth among the bloggers. If Richard is using a product and speaks highly of it, I'm likely to take a look. Especially if he says it fits into what I'm doing through the open formats my software already supports.

\n

Then I want a river, a place where I can go to find out quickly what's new, in the way of products, not BigCo bluster or another $250 million VC deal. I want to know what my peers are doing. So I can learn from them, and so we can make our products work with theirs.

\n

Work together is a phrase you'll hear me use a lot. It's the potential of tech, but it often isn't the attitude of tech. Even the smallest most independent developers dream of dominating. You can't work with people who dominate, even if they win.

\n

I want to hear about products that are open to connecting to mine.

\n

There have been times, often defined by news sources, that have created huge swells of compatible technology. To name a few: InfoWorld, PC WEEK, MacWEEK, TechCrunch. Very fond memories of the communities that gathered around each of those.

\n

It's time for another. The opportunity is there. It's been a long time since we had an open development community that worked to create great new user experience without lockin. It's like riding a bicycle or swimming, you don't forget how to do it. And like tennis or baseball, you can't play without partners and competition.

\n", + "pubDate": "Sat, 24 Jun 2017 13:43:48 GMT", + "guid": "http://scripting.com/2017/06/24.html#a090648", + "source:outline": { + "text": "The sad state of tech news in 2017", + "created": "Sat, 24 Jun 2017 13:43:48 GMT", + "type": "outline", + "subs": [ + { + "text": "It won't take much to reboot the tech blogosphere, just a few bloggers with ideas who listen to each other and want to work with each other. That was the idea behind blogrolls, to visibly show the relationships. ", + "created": "Sat, 24 Jun 2017 13:43:55 GMT", + "subs": [ + { + "text": "I'm still thinking about how to integrate a blogroll with the new design of \"Scripting News\". I pushed everything aside to get a clean look for the new site. I want to avoid bringing it all back. ", + "created": "Sat, 24 Jun 2017 14:32:17 GMT", + "permalink": "http://scripting.com/2017/06/24.html#a100617" + } + ], + "permalink": "http://scripting.com/2017/06/24.html#a090655" + }, + { + "text": "So, we have a few people who are writing and listening. That's a needed first step. Next we need a way to announce and hear about new tech products. Not just ones that get VC backing or come from big companies. We already hear about those products through \"TechMeme\" and the pubs that contribute to it. We also have platform-specific news about tech products, it's more limited, but it's there.", + "created": "Sat, 24 Jun 2017 13:44:09 GMT", + "permalink": "http://scripting.com/2017/06/24.html#a090609" + }, + { + "text": "It will likely start with word of mouth among the bloggers. If Richard is using a product and speaks highly of it, I'm likely to take a look. Especially if he says it fits into what I'm doing through the open formats my software already supports. ", + "created": "Sat, 24 Jun 2017 13:52:04 GMT", + "permalink": "http://scripting.com/2017/06/24.html#a090604" + }, + { + "text": "Then I want a river, a place where I can go to find out quickly what's new, in the way of products, not BigCo bluster or another $250 million VC deal. I want to know what my peers are doing. So I can learn from them, and so we can make our products work with theirs. ", + "created": "Sat, 24 Jun 2017 13:52:04 GMT", + "permalink": "http://scripting.com/2017/06/24.html#a090604" + }, + { + "text": "Work together is a phrase you'll hear me use a lot. It's the potential of tech, but it often isn't the attitude of tech. Even the smallest most independent developers dream of dominating. You can't work with people who dominate, even if they win. ", + "created": "Sat, 24 Jun 2017 13:54:33 GMT", + "permalink": "http://scripting.com/2017/06/24.html#a090633" + }, + { + "text": "I want to hear about products that are open to connecting to mine. ", + "created": "Sat, 24 Jun 2017 20:53:41 GMT", + "permalink": "http://scripting.com/2017/06/24.html#a040641" + }, + { + "text": "There have been times, often defined by news sources, that have created huge swells of compatible technology. To name a few: InfoWorld, PC WEEK, MacWEEK, TechCrunch. Very fond memories of the communities that gathered around each of those. ", + "created": "Sat, 24 Jun 2017 13:48:02 GMT", + "permalink": "http://scripting.com/2017/06/24.html#a090602" + }, + { + "text": "It's time for another. The opportunity is there. It's been a long time since we had an open development community that worked to create great new user experience without lockin. It's like riding a bicycle or swimming, you don't forget how to do it. And like tennis or baseball, you can't play without partners and competition. ", + "created": "Sat, 24 Jun 2017 13:57:02 GMT", + "permalink": "http://scripting.com/2017/06/24.html#a090602" + } + ], + "permalink": "http://scripting.com/2017/06/24.html#a090648" + } + }, + { + "title": "Republican-inspired art", + "link": "http://scripting.com/2017/06/24.html#a100632", + "description": "

\"A

\n", + "pubDate": "Sat, 24 Jun 2017 14:52:32 GMT", + "guid": "http://scripting.com/2017/06/24.html#a100632", + "source:outline": { + "text": "Republican-inspired art", + "created": "Sat, 24 Jun 2017 14:52:32 GMT", + "type": "outline", + "subs": [ + { + "text": "\"A", + "created": "Sat, 24 Jun 2017 14:52:37 GMT", + "permalink": "http://scripting.com/2017/06/24.html#a100637" + } + ], + "permalink": "http://scripting.com/2017/06/24.html#a100632" + } + }, + { + "link": "http://scripting.com/2017/06/23.html#a070648", + "description": "Thank you Om for the tweet-love. ❤️ ", + "pubDate": "Fri, 23 Jun 2017 11:50:48 GMT", + "guid": "http://scripting.com/2017/06/23.html#a070648", + "source:outline": { + "text": "Thank you Om for the tweet-love. :heart: ", + "created": "Fri, 23 Jun 2017 11:50:48 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/23.html#a070648" + } + }, + { + "link": "http://scripting.com/2017/06/23.html#a120634", + "description": "An idea worth RT'ing: \"A site people could go to, fill in some info about themselves, and find out how much they would lose under the Repub plan.\"", + "pubDate": "Fri, 23 Jun 2017 16:02:34 GMT", + "guid": "http://scripting.com/2017/06/23.html#a120634", + "source:outline": { + "text": "An idea worth RT'ing: \"A site people could go to, fill in some info about themselves, and find out how much they would lose under the Repub plan.\"", + "created": "Fri, 23 Jun 2017 16:02:34 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/23.html#a120634" + } + }, + { + "link": "http://scripting.com/2017/06/23.html#a090638", + "description": "githubpub is a Node app that serves from GitHub repositories.", + "pubDate": "Fri, 23 Jun 2017 13:52:38 GMT", + "guid": "http://scripting.com/2017/06/23.html#a090638", + "source:outline": { + "text": "\"githubpub\" is a Node app that serves from GitHub repositories.", + "created": "Fri, 23 Jun 2017 13:52:38 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/23.html#a090638" + } + }, + { + "link": "http://scripting.com/2017/06/23.html#a100602", + "description": "Fix for the Scripting News RSS feed: we now process glossary entries and emoji short codes. The net effect is that text shortcuts like RSS will be expanded as well as 👏 emoji 👏, in the feeds. As they say, still diggin!", + "pubDate": "Fri, 23 Jun 2017 14:52:02 GMT", + "guid": "http://scripting.com/2017/06/23.html#a100602", + "source:outline": { + "text": "Fix for the Scripting News RSS feed: we now process glossary entries and emoji short codes. The net effect is that text shortcuts like \"rss\" will be expanded as well as :clap: emoji :clap:, in the feeds. As they say, still diggin!", + "created": "Fri, 23 Jun 2017 14:52:02 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/23.html#a100602" + } + }, + { + "link": "http://scripting.com/2017/06/23.html#a070659", + "description": "So many thoughts today begin with -- If the Dems only had their shit together. ", + "pubDate": "Fri, 23 Jun 2017 11:52:59 GMT", + "guid": "http://scripting.com/2017/06/23.html#a070659", + "source:outline": { + "text": "So many thoughts today begin with -- If the Dems only had their shit together. ", + "created": "Fri, 23 Jun 2017 11:52:59 GMT", + "type": "tweet", + "tweetId": "878220762343133184", + "tweetUserName": "davewiner", + "permalink": "http://scripting.com/2017/06/23.html#a070659" + } + }, + { + "link": "http://scripting.com/2017/06/23.html#a070624", + "description": "If the Dems only had their shit together, we would be mobleizable to knock on neighbor's doors this weekend with pre-written talking points. \"Did you know that you could will lose your health insurance if the Republicans have their way?\"", + "pubDate": "Fri, 23 Jun 2017 11:53:24 GMT", + "guid": "http://scripting.com/2017/06/23.html#a070624", + "source:outline": { + "text": "If the Dems only had their shit together, we would be mobleizable to knock on neighbor's doors this weekend with pre-written talking points. \"Did you know that you could will lose your health insurance if the Republicans have their way?\"", + "created": "Fri, 23 Jun 2017 11:53:24 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/23.html#a070624" + } + }, + { + "link": "http://scripting.com/2017/06/23.html#a070633", + "description": "Repubs who consider voting for the repeal of Medicaid and the ACA should fear the hellfire they will face when they run for re-election. This weekend is the time to make your feelings felt. ", + "pubDate": "Fri, 23 Jun 2017 11:47:33 GMT", + "guid": "http://scripting.com/2017/06/23.html#a070633", + "source:outline": { + "text": "Repubs who consider voting for the repeal of Medicaid and the ACA should fear the hellfire they will face when they run for re-election. This weekend is the time to make your feelings felt. ", + "created": "Fri, 23 Jun 2017 11:47:33 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/23.html#a070633" + } + }, + { + "link": "http://scripting.com/2017/06/23.html#a070641", + "description": "BTW, if someone expresses frustration it doesn't follow that they blame you, esp on Twitter which is a length-impaired medium. ", + "pubDate": "Fri, 23 Jun 2017 11:45:41 GMT", + "guid": "http://scripting.com/2017/06/23.html#a070641", + "source:outline": { + "text": "BTW, if someone expresses frustration it doesn't follow that they blame you, esp on Twitter which is a length-impaired medium. ", + "created": "Fri, 23 Jun 2017 11:45:41 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/23.html#a070641" + } + }, + { + "title": "Thin servers", + "link": "http://scripting.com/2017/06/23.html#a100620", + "description": "

Two friends, Jon Udell and Mike Caulfield, are talking about \"dumb\" servers. I call the same things \"thin.\" Also fractional-horsepower servers. They go by a bunch of names, but the idea and motivation is the same.

\n

The idea: We move functionality from the server to the edge (desktop, mobile device), repeating until someday there's nothing left on the server. We could go all the way, but it needs a strong operational backend, something a big company is good at, not so much individuals. (With the caveat that some think this problem is distributable, notably the fictional CEO of Pied Piper.)

\n

The key thing is identity. Once you have that solved, it all becomes relatively easy. I've factored out identity into a layer I call nodeStorage. It associates storage with a user's Twitter identity. Twitter is a good service to use, unlike some others, because they have a liberal policy of who gets to create apps. Faceook has an extensive vetting process. Twitter is \"let a thousand flowers bloom.\" I know some people have problems with Twitter, but I've learned over many years that all corporate vendors are imperfect. If you're waiting for perfection you'll wait forever. And you build the software so that if Twitter should again become draconian, a new service can be filled in with as little disruption as possible.

\n

Another place I've looked is Dropbox. There, with one simple feature, the ability to associate a domain with a folder, they would solve the problem. I know there are external services that provide something \"like\" this, but fundamentally Dropbox doesn't provide enough flexibility in the API to do this in a reasonable way. (Lack of granularity in permissions, an app gets access to one folder or everything.)

\n

Or Amazon, if their identity system for AWS were simpler for end users, or if their end-user storage system could be accessed through the S3 API. I'm sure they've thought of it. There must be a reason they don't do it.

\n

And Twitter could completely eliminate the need for nodeStorage, by offering users a few gigabytes of storage attached to their Twitter account, accessible through the API. The first person who described the feature to me was Jack Dorsey, about eight years ago, when we met for coffee in SF. So he understands why this idea is so powerful. I'm not sure what the holdup is.

\n

In the meantime, nodeStorage works. I build the kind of apps I want. Open the sidebar on Scripting News (left margin) and have a look at the apps. Some even have source code so you can see for yourself.

\n", + "pubDate": "Fri, 23 Jun 2017 14:03:20 GMT", + "guid": "http://scripting.com/2017/06/23.html#a100620", + "source:outline": { + "text": "Thin servers", + "created": "Fri, 23 Jun 2017 14:03:20 GMT", + "type": "outline", + "subs": [ + { + "text": "Two friends, Jon Udell and Mike Caulfield, are talking about \"dumb\" servers. I call the same things \"thin.\" Also fractional-horsepower servers. They go by a bunch of names, but the idea and motivation is the same. ", + "created": "Fri, 23 Jun 2017 14:05:30 GMT", + "permalink": "http://scripting.com/2017/06/23.html#a100630" + }, + { + "text": "The idea: We move functionality from the server to the edge (desktop, mobile device), repeating until someday there's nothing left on the server. We could go all the way, but it needs a strong operational backend, something a big company is good at, not so much individuals. (With the caveat that some think this problem is distributable, notably the fictional CEO of Pied Piper.)", + "created": "Fri, 23 Jun 2017 14:24:35 GMT", + "permalink": "http://scripting.com/2017/06/23.html#a100635" + }, + { + "text": "The key thing is identity. Once you have that solved, it all becomes relatively easy. I've factored out identity into a layer I call \"nodeStorage\". It associates storage with a user's Twitter identity. Twitter is a good service to use, unlike some others, because they have a liberal policy of who gets to create apps. Faceook has an extensive vetting process. Twitter is \"let a thousand flowers bloom.\" I know some people have problems with Twitter, but I've learned over many years that all corporate vendors are imperfect. If you're waiting for perfection you'll wait forever. And you build the software so that if Twitter should again become draconian, a new service can be filled in with as little disruption as possible.", + "created": "Fri, 23 Jun 2017 14:05:43 GMT", + "image": "http://scripting.com/images/2017/06/17/bowling.png", + "permalink": "http://scripting.com/2017/06/23.html#a100643" + }, + { + "text": "Another place I've looked is Dropbox. There, with one simple feature, the ability to associate a domain with a folder, they would solve the problem. I know there are external services that provide something \"like\" this, but fundamentally Dropbox doesn't provide enough flexibility in the API to do this in a reasonable way. (Lack of granularity in permissions, an app gets access to one folder or everything.)", + "created": "Fri, 23 Jun 2017 14:08:41 GMT", + "permalink": "http://scripting.com/2017/06/23.html#a100641" + }, + { + "text": "Or Amazon, if their identity system for AWS were simpler for end users, or if their end-user storage system could be accessed through the S3 API. I'm sure they've thought of it. There must be a reason they don't do it. ", + "created": "Fri, 23 Jun 2017 14:09:10 GMT", + "permalink": "http://scripting.com/2017/06/23.html#a100610" + }, + { + "text": "And Twitter could completely eliminate the need for nodeStorage, by offering users a few gigabytes of storage attached to their Twitter account, accessible through the API. The first person who described the feature to me was Jack Dorsey, about eight years ago, when we met for coffee in SF. So he understands why this idea is so powerful. I'm not sure what the holdup is. ", + "created": "Fri, 23 Jun 2017 14:09:55 GMT", + "permalink": "http://scripting.com/2017/06/23.html#a100655" + }, + { + "text": "In the meantime, \"nodeStorage\" works. I build the kind of apps I want. Open the sidebar on Scripting News (left margin) and have a look at the apps. Some even have source code so you can see for yourself. ", + "created": "Fri, 23 Jun 2017 14:33:10 GMT", + "permalink": "http://scripting.com/2017/06/23.html#a100610" + } + ], + "permalink": "http://scripting.com/2017/06/23.html#a100620" + } + }, + { + "title": "RicMac, part II", + "link": "http://scripting.com/2017/06/23.html#a070643", + "description": "

Richard MacManus keeps on truckin. There's nothing more powerful than a persistent and curious user who's relatively fearless.

\n

In a follow-up post I learned that there is an IndieWeb-approved feed reader called Woodwind. That's good news. RSS and related technolgies, including OPML import and export, are essential components of the open web.

\n

BTW, to Richard, I wrote up my rules for standards-makers, based on experience re what (imho) is important and what works and doesn't. Another item for your consideration.

\n", + "pubDate": "Fri, 23 Jun 2017 11:14:43 GMT", + "guid": "http://scripting.com/2017/06/23.html#a070643", + "source:outline": { + "text": "RicMac, part II", + "created": "Fri, 23 Jun 2017 11:14:43 GMT", + "type": "outline", + "subs": [ + { + "text": "Richard MacManus keeps on truckin. There's nothing more powerful than a persistent and curious user who's relatively fearless. ", + "created": "Fri, 23 Jun 2017 11:20:38 GMT", + "image": "http://scripting.com/images/2017/06/23/mrNatural.png", + "permalink": "http://scripting.com/2017/06/23.html#a070638" + }, + { + "text": "In a follow-up post I learned that there is an IndieWeb-approved feed reader called Woodwind. That's good news. \"RSS\" and related technolgies, including OPML import and export, are essential components of the open web. ", + "created": "Fri, 23 Jun 2017 11:15:54 GMT", + "permalink": "http://scripting.com/2017/06/23.html#a070654" + }, + { + "text": "BTW, to Richard, I wrote up my rules for standards-makers, based on experience re what (imho) is important and what works and doesn't. Another item for your consideration. ", + "created": "Fri, 23 Jun 2017 11:19:38 GMT", + "permalink": "http://scripting.com/2017/06/23.html#a070638" + } + ], + "permalink": "http://scripting.com/2017/06/23.html#a070643" + } + }, + { + "title": "Test post", + "link": "http://scripting.com/2017/06/23.html#a030621", + "description": "

Here's a list with four items

\n
    \n\t
  • one
  • \n\t
  • two
  • \n\t
  • three
  • \n\t
  • four
  • \n\t
\n", + "pubDate": "Fri, 23 Jun 2017 19:48:21 GMT", + "guid": "http://scripting.com/2017/06/23.html#a030621", + "source:outline": { + "text": "Test post", + "created": "Fri, 23 Jun 2017 19:48:21 GMT", + "type": "outline", + "subs": [ + { + "text": "Here's a list with four items", + "created": "Fri, 23 Jun 2017 19:49:21 GMT", + "flNumberedSubs": "true", + "subs": [ + { + "text": "one", + "created": "Fri, 23 Jun 2017 19:48:43 GMT", + "permalink": "http://scripting.com/2017/06/23.html#a030643" + }, + { + "text": "two", + "created": "Fri, 23 Jun 2017 19:48:44 GMT", + "permalink": "http://scripting.com/2017/06/23.html#a030644" + }, + { + "text": "three", + "created": "Fri, 23 Jun 2017 19:48:45 GMT", + "permalink": "http://scripting.com/2017/06/23.html#a030645" + }, + { + "text": "four", + "created": "Fri, 23 Jun 2017 19:48:46 GMT", + "permalink": "http://scripting.com/2017/06/23.html#a030646" + } + ], + "permalink": "http://scripting.com/2017/06/23.html#a030621" + } + ], + "permalink": "http://scripting.com/2017/06/23.html#a030621" + } + }, + { + "link": "http://scripting.com/2017/06/22.html#a120633", + "description": "I'm working on a new Node web server that serves out of GitHub repos. It's a very sweet very small piece of software. ", + "pubDate": "Thu, 22 Jun 2017 16:28:33 GMT", + "guid": "http://scripting.com/2017/06/22.html#a120633", + "source:outline": { + "text": "I'm working on a new Node web server that serves out of GitHub repos. It's a very sweet very small piece of software. ", + "created": "Thu, 22 Jun 2017 16:28:33 GMT", + "type": "tweet", + "tweetId": "877926320822464512", + "tweetUserName": "davewiner", + "permalink": "http://scripting.com/2017/06/22.html#a120633" + } + }, + { + "link": "http://scripting.com/2017/06/22.html#a090616", + "description": "An epiphany. Mark Zuckerberg is his generation's Ray Kroc, and Facebook is McDonald's. I aspire to be Alice Waters.", + "pubDate": "Thu, 22 Jun 2017 13:29:16 GMT", + "guid": "http://scripting.com/2017/06/22.html#a090616", + "source:outline": { + "text": "An epiphany. Mark Zuckerberg is his generation's Ray Kroc, and Facebook is McDonald's. I aspire to be Alice Waters.", + "created": "Thu, 22 Jun 2017 13:29:16 GMT", + "type": "tweet", + "tweetId": "877881227298000896", + "tweetUserName": "davewiner", + "permalink": "http://scripting.com/2017/06/22.html#a090616" + } + }, + { + "link": "http://scripting.com/2017/06/22.html#a090644", + "description": "Future-of-journalism conferences that ignore blogging are not about the future of journalism.", + "pubDate": "Thu, 22 Jun 2017 13:29:44 GMT", + "guid": "http://scripting.com/2017/06/22.html#a090644", + "source:outline": { + "text": "Future-of-journalism conferences that ignore blogging are not about the future of journalism.", + "created": "Thu, 22 Jun 2017 13:29:44 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/22.html#a090644" + } + }, + { + "link": "http://scripting.com/2017/06/22.html#a120629", + "description": "News will be interesting tonight. They've got the Repub health care bill to rip apart, and it's also NBA Draft night. ", + "pubDate": "Thu, 22 Jun 2017 16:27:29 GMT", + "guid": "http://scripting.com/2017/06/22.html#a120629", + "source:outline": { + "text": "News will be interesting tonight. They've got the Repub health care bill to rip apart, and it's also NBA Draft night. ", + "created": "Thu, 22 Jun 2017 16:27:29 GMT", + "type": "tweet", + "tweetId": "877926057260728320", + "tweetUserName": "davewiner", + "permalink": "http://scripting.com/2017/06/22.html#a120629" + } + }, + { + "title": "Fargo puzzler", + "link": "http://scripting.com/2017/06/22.html#a030610", + "description": "

The last two episodes of season 3 of Fargo were fantastic. But, the opening scene of episode 1, which takes place in a police office in East Germany during the Cold War, is without explanation.

\n

All through the season, I was wondering how it was going to be connected up with the story that takes place in Minnesota in 2011, but as far as I know it never was.

\n

Maybe that was VM Varga as the accused? Or the police guy?

\n

This is kind of bothering me! :-)

\n

Okay then...

\n

Update: In the episode guide on Wikipedia they describe the opening scene as follows: \"In 1988 East Berlin, Jacob Ungerleider is questioned in the death of a woman, which he claims is a case of mistaken identity.\" So it's not VM Varga in the hot seat. Who is Jacob Ungerleider? I have no idea! ;-)

\n", + "pubDate": "Thu, 22 Jun 2017 19:26:10 GMT", + "guid": "http://scripting.com/2017/06/22.html#a030610", + "source:outline": { + "text": "Fargo puzzler", + "created": "Thu, 22 Jun 2017 19:26:10 GMT", + "type": "outline", + "subs": [ + { + "text": "The last two episodes of season 3 of Fargo were fantastic. But, the opening scene of episode 1, which takes place in a police office in East Germany during the Cold War, is without explanation. ", + "created": "Thu, 22 Jun 2017 19:26:18 GMT", + "image": "http://scripting.com/images/2017/06/22/paulBunyan.png", + "permalink": "http://scripting.com/2017/06/22.html#a030618" + }, + { + "text": "All through the season, I was wondering how it was going to be connected up with the story that takes place in Minnesota in 2011, but as far as I know it never was.", + "created": "Thu, 22 Jun 2017 19:27:20 GMT", + "permalink": "http://scripting.com/2017/06/22.html#a030620" + }, + { + "text": "Maybe that was VM Varga as the accused? Or the police guy?", + "created": "Thu, 22 Jun 2017 19:27:54 GMT", + "permalink": "http://scripting.com/2017/06/22.html#a030654" + }, + { + "text": "This is kind of bothering me! :-)", + "created": "Thu, 22 Jun 2017 19:28:15 GMT", + "permalink": "http://scripting.com/2017/06/22.html#a030615" + }, + { + "text": "Okay then...", + "created": "Thu, 22 Jun 2017 19:28:27 GMT", + "permalink": "http://scripting.com/2017/06/22.html#a030627" + }, + { + "text": "Update: In the episode guide on Wikipedia they describe the opening scene as follows: \"In 1988 East Berlin, Jacob Ungerleider is questioned in the death of a woman, which he claims is a case of mistaken identity.\" So it's not VM Varga in the hot seat. Who is Jacob Ungerleider? I have no idea! ;-)", + "created": "Thu, 22 Jun 2017 19:38:25 GMT", + "permalink": "http://scripting.com/2017/06/22.html#a030625" + } + ], + "permalink": "http://scripting.com/2017/06/22.html#a030610" + } + }, + { + "link": "http://scripting.com/2017/06/21.html#a060647", + "description": "Good morning summer solstice fans!", + "pubDate": "Wed, 21 Jun 2017 10:21:47 GMT", + "guid": "http://scripting.com/2017/06/21.html#a060647", + "source:outline": { + "text": "Good morning summer solstice fans!", + "created": "Wed, 21 Jun 2017 10:21:47 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/21.html#a060647" + } + }, + { + "link": "http://scripting.com/2017/06/21.html#a060611", + "description": "It was a boring NBA postseason, for the most. But the excitement of next season is already starting, with the draft tomorrow, and deal season in full swing. The place to find all the news is nbariver.com. It's one of many rivers maintained by my River5 installation. ", + "pubDate": "Wed, 21 Jun 2017 10:22:11 GMT", + "guid": "http://scripting.com/2017/06/21.html#a060611", + "source:outline": { + "text": "It was a boring NBA postseason, for the most. But the excitement of next season is already starting, with the draft tomorrow, and deal season in full swing. The place to find all the news is nbariver.com. It's one of many rivers maintained by my \"River5\" installation. ", + "created": "Wed, 21 Jun 2017 10:22:11 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/21.html#a060611" + } + }, + { + "link": "http://scripting.com/2017/06/21.html#a060632", + "description": "BTW, I hate the term \"eating the dogfood.\" As much as I love dogs, it says that our users are pets, not sentient human beings, our equals. It also says our software is dog food. I think as a kid, as an experiment, a few of us kids actually ate dog food. It's a vague memory, that must have some basis in reality. It makes me nauseous to think about it. And that's what I think about when I hear the term. Please, let's find another way of saying \"My software is good because I use it, and vice versa.\"", + "pubDate": "Wed, 21 Jun 2017 10:57:32 GMT", + "guid": "http://scripting.com/2017/06/21.html#a060632", + "source:outline": { + "text": "BTW, I hate the term \"eating the dogfood.\" As much as I love dogs, it says that our users are pets, not sentient human beings, our equals. It also says our software is dog food. I think as a kid, as an experiment, a few of us kids actually ate dog food. It's a vague memory, that must have some basis in reality. It makes me nauseous to think about it. And that's what I think about when I hear the term. Please, let's find another way of saying \"My software is good because I use it, and vice versa.\"", + "created": "Wed, 21 Jun 2017 10:57:32 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/21.html#a060632" + } + }, + { + "link": "http://scripting.com/2017/06/21.html#a070633", + "description": "Yesterday I posted a screen shot of one of my posts on Facebook, to accolades from friends on Facebook. I deleted the post. I won't be doing it again. Facebook is not a place for blog posts. Not as long as they disable linking, styles, titles and podcasts. If you want to help Facebook destroy the open web, go for it. But I will not participate in that awful adventure.", + "pubDate": "Wed, 21 Jun 2017 11:01:33 GMT", + "guid": "http://scripting.com/2017/06/21.html#a070633", + "source:outline": { + "text": "Yesterday I posted a screen shot of one of my posts on Facebook, to accolades from friends on Facebook. I deleted the post. I won't be doing it again. Facebook is not a place for blog posts. Not as long as they disable linking, styles, titles and podcasts. If you want to help Facebook destroy the open web, go for it. But I will not participate in that awful adventure.", + "created": "Wed, 21 Jun 2017 11:01:33 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/21.html#a070633" + } + }, + { + "link": "http://scripting.com/2017/06/21.html#a060600", + "description": "On Facebook you are who the algorithm says you are. ", + "pubDate": "Wed, 21 Jun 2017 10:53:00 GMT", + "guid": "http://scripting.com/2017/06/21.html#a060600", + "source:outline": { + "text": "On Facebook you are who the algorithm says you are. ", + "created": "Wed, 21 Jun 2017 10:53:00 GMT", + "type": "outline", + "permalink": "http://scripting.com/2017/06/21.html#a060600" + } + }, + { + "title": "GitHub API example app", + "link": "http://scripting.com/2017/06/21.html#a110614", + "description": "

A simple web app that travels through the River5 repository in my GitHub account, producing a directory that reflects the structure of the repo.

\n

I couldn't find sample code that does this simple thing. Now I won't have to hunt for it, and neither will you. ;-)

\n

Here's the source code.

\n", + "pubDate": "Wed, 21 Jun 2017 15:18:14 GMT", + "guid": "http://scripting.com/2017/06/21.html#a110614", + "source:outline": { + "text": "GitHub API example app", + "created": "Wed, 21 Jun 2017 15:18:14 GMT", + "type": "outline", + "subs": [ + { + "text": "A simple web app that travels through the River5 repository in my GitHub account, producing a directory that reflects the structure of the repo.", + "created": "Wed, 21 Jun 2017 15:18:24 GMT", + "permalink": "http://scripting.com/2017/06/21.html#a110624" + }, + { + "text": "I couldn't find sample code that does this simple thing. Now I won't have to hunt for it, and neither will you. ;-) ", + "created": "Wed, 21 Jun 2017 15:18:36 GMT", + "permalink": "http://scripting.com/2017/06/21.html#a110636" + }, + { + "text": "Here's the source code. ", + "created": "Wed, 21 Jun 2017 15:19:39 GMT", + "permalink": "http://scripting.com/2017/06/21.html#a110639" + } + ], + "permalink": "http://scripting.com/2017/06/21.html#a110614" + } + }, + { + "title": "Bike video from two years ago", + "link": "http://scripting.com/2017/06/21.html#a020637", + "description": "

\n", + "pubDate": "Wed, 21 Jun 2017 18:47:37 GMT", + "guid": "http://scripting.com/2017/06/21.html#a020637", + "source:outline": { + "text": "Bike video from two years ago", + "created": "Wed, 21 Jun 2017 18:47:37 GMT", + "type": "outline", + "subs": [ + { + "text": "", + "created": "Wed, 21 Jun 2017 18:47:50 GMT", + "permalink": "http://scripting.com/2017/06/21.html#a020650" + } + ], + "permalink": "http://scripting.com/2017/06/21.html#a020637" + } + }, + { + "title": "An old friend: Richard MacManus", + "link": "http://scripting.com/2017/06/21.html#a060651", + "description": "

Richard is one of the old school bloggers. He started ReadWriteWeb in 2003. It started as a Radio UserLand project and grew into a leading tech publication, something which I'm personally proud of.

\n

He has a new blog up and running. I've added it to my personal river here on Scripting News. He asks about where the blogrolls have gone, a topic I wrote about a couple of days ago. Richard would certainly be in my blogroll.

\n

Maybe the subscription list for my blogger's river would make a good start for my blogroll, or vice versa? Something we didn't do in the first iteration is make our rivers public. Nowadays I'm doing that routinely. A few examples are in the left sidebar here on Scripting News.

\n

Richard has turned to IndieWeb for the latest on open web tech. That's fine, but you have to look elsewhere too, because as he's discovered, they only embrace part of the open web. It's too bad they chose such an inclusive name, but have an exclusive approach. For example, they have avoided RSS, for reasons I'm sure I don't understand (I've listened, so no need to repeat the reasoning). We need all the advantages we can get because there are serious headwinds these days for blogging. RSS is serious open web technology. To not build on it is unthinkable, for me at least.

\n

Re integration between writing and reading, another topic of interest to Richard, all my rivers hook into Radio3, which is my latest linkblogging tool. For reading, I encouraged Richard to look at Electric River, it's the closest to what Radio UserLand did with aggregation in 2002. It runs on your Mac desktop, as the original did. When he wants to go all-in with rivers, nothing can take the place of River5, which is getting both modular and deep. I'm doing more work on that. Rivers have not finished evolving as far as I'm concerned.

\n", + "pubDate": "Wed, 21 Jun 2017 10:29:51 GMT", + "guid": "http://scripting.com/2017/06/21.html#a060651", + "source:outline": { + "text": "An old friend: Richard MacManus", + "created": "Wed, 21 Jun 2017 10:29:51 GMT", + "type": "outline", + "subs": [ + { + "text": "Richard is one of the old school bloggers. He started ReadWriteWeb in 2003. It started as a Radio UserLand project and grew into a leading tech publication, something which I'm personally proud of. ", + "created": "Wed, 21 Jun 2017 10:33:59 GMT", + "image": "http://scripting.com/2016/03/10/clown.png", + "permalink": "http://scripting.com/2017/06/21.html#a060659" + }, + { + "text": "He has a new blog up and running. I've added it to my personal river here on Scripting News. He asks about where the blogrolls have gone, a topic I wrote about a couple of days ago. Richard would certainly be in my blogroll.", + "created": "Wed, 21 Jun 2017 10:25:16 GMT", + "permalink": "http://scripting.com/2017/06/21.html#a060616" + }, + { + "text": "Maybe the subscription list for my blogger's river would make a good start for my blogroll, or vice versa? Something we didn't do in the first iteration is make our rivers public. Nowadays I'm doing that routinely. A few examples are in the left sidebar here on \"Scripting News\". ", + "created": "Wed, 21 Jun 2017 10:40:31 GMT", + "permalink": "http://scripting.com/2017/06/21.html#a060631" + }, + { + "text": "Richard has turned to IndieWeb for the latest on open web tech. That's fine, but you have to look elsewhere too, because as he's discovered, they only embrace part of the open web. It's too bad they chose such an inclusive name, but have an exclusive approach. For example, they have avoided \"RSS\", for reasons I'm sure I don't understand (I've listened, so no need to repeat the reasoning). We need all the advantages we can get because there are serious headwinds these days for blogging. RSS is serious open web technology. To not build on it is unthinkable, for me at least. ", + "created": "Wed, 21 Jun 2017 10:30:33 GMT", + "permalink": "http://scripting.com/2017/06/21.html#a060633" + }, + { + "text": "Re integration between writing and reading, another topic of interest to Richard, all my rivers hook into \"Radio3\", which is my latest linkblogging tool. For reading, I encouraged Richard to look at \"Electric River\", it's the closest to what Radio UserLand did with aggregation in 2002. It runs on your Mac desktop, as the original did. When he wants to go all-in with rivers, nothing can take the place of \"River5\", which is getting both modular and deep. I'm doing more work on that. Rivers have not finished evolving as far as I'm concerned. ", + "created": "Wed, 21 Jun 2017 10:32:27 GMT", + "permalink": "http://scripting.com/2017/06/21.html#a060627" + } + ], + "permalink": "http://scripting.com/2017/06/21.html#a060651" + } + }, + { + "link": "http://scripting.com/2017/06/20.html#a100647", + "description": "Anyone want to blog-debate about XML vs JSON? I've spent years using both, I think I have an objective view of the strengths of each. Imho, they are almost the same thing. XML has attributes and values, and that does make it more complex. Slightly. But you don't have to use the extra features. Look at OPML for an idea of a simple very JSON-like application of XML. Beyond that, there's really no difference. If you disagree, write a post, link to this and send me the link. I will read what you wrote, and respond, on my blog, if I have something to say. There's been so much bullshit flying around. I'd like to cut through that. ", + "pubDate": "Wed, 21 Jun 2017 02:56:47 GMT", + "guid": "http://scripting.com/2017/06/20.html#a100647", + "source:outline": { + "text": "Anyone want to blog-debate about XML vs JSON? I've spent years using both, I think I have an objective view of the strengths of each. Imho, they are almost the same thing. XML has attributes and values, and that does make it more complex. Slightly. But you don't have to use the extra features. Look at \"OPML\" for an idea of a simple very JSON-like application of XML. Beyond that, there's really no difference. If you disagree, write a post, link to this and send me the link. I will read what you wrote, and respond, on my blog, if I have something to say. There's been so much bullshit flying around. I'd like to cut through that. ", + "created": "Wed, 21 Jun 2017 02:56:47 GMT", + "type": "outline", + "image": "http://scripting.com/images/2017/06/20/penny.png", + "permalink": "http://scripting.com/2017/06/20.html#a100647" + } + } + ] + } + } +} \ No newline at end of file diff --git a/Frameworks/RSParser/RSParserTests/Resources/inessential.json b/Frameworks/RSParser/RSParserTests/Resources/inessential.json new file mode 100644 index 000000000..b1596032a --- /dev/null +++ b/Frameworks/RSParser/RSParserTests/Resources/inessential.json @@ -0,0 +1,156 @@ +{ + "version": "https://jsonfeed.org/version/1", + "title": "inessential.com", + "description": "Brent Simmons’s weblog.", + "home_page_url": "http://inessential.com/", + "feed_url": "http://inessential.com/feed.json", + "user_comment": "This feed allows you to read the posts from this site in any feed reader that supports the JSON Feed format. To add this feed to your reader, copy the following URL — http://inessential.com/feed.json — and add it your reader.", + "favicon": "http://inessential.com/favicon.ico", + "author": { + "name": "Brent Simmons", + "url": "http://inessential.com/", + "avatar": "http://ranchero.com/downloads/brent_avatar.png" + }, + "items": [ + { + "id": "http://inessential.com/2017/06/02/james_dempsey_and_the_breakpoints_benefi", + "url": "http://inessential.com/2017/06/02/james_dempsey_and_the_breakpoints_benefi", + "title": "James Dempsey and the Breakpoints Benefit App Camp for Girls", + "content_html": "

On Wednesday night I know where I’ll be — playing keyboard for a few songs at the James Dempsey and the Breakpoints concert benefitting App Camp for Girls.

\n\n

You should get tickets. It’s a fun time for a great cause.

\n\n

Bonus: James writes about how this concert is full circle for him. It’s a special night.

", + "date_published": "2017-06-02T22:05:47-07:00" + }, + { + "id": "http://inessential.com/2017/06/01/evergreen_diary_1_open_source", + "url": "http://inessential.com/2017/06/01/evergreen_diary_1_open_source", + "title": "Evergreen Diary #1: Open Source", + "content_html": "

Evergreen is a new feed reader for Macs. It’s not actually done yet — in fact, it’s not even alpha yet, much less beta. It’s still in the painful-to-use stage, for sure.

\n\n

I’ve been working on it (among other things) on nights and weekends for a couple years. For much of the time I planned to make it a for-pay app — the plan was a free Lite version and a for-pay version.

\n\n

But as time went on I was less and less motivated to make a for-pay app. Doing all that stuff — dealing with licenses, money, a store, support, and everything else that goes along with a commercial app — just didn’t sound like any fun, and it would have taken time away from actually working on the app, which is all I really want to do. I just don’t have time to spare.

\n\n

So I decided to make it free and open source. (The code is up on GitHub.) This fits with my goals:

\n\n
    \n
  • Promoting feed-reading as part of promoting the open web.
  • \n
  • Publishing a bunch of feed-reading code and an example Mac app that other developers can use.
  • \n
  • Giving me something to write about on this blog.
  • \n
\n\n\n

I like developing in public. Publishing the code makes it feel like a performance, a kind of tightwire act. Which suits me.

\n\n

* * *

\n\n\n

The one thing that almost held me back from making it open source was the effect on other developers. There are for-pay Mac feed readers, after all, and I don’t want to take anything away from them.

\n\n

And I don’t want to send the message that software ought to cost nothing.

\n\n

I think that making it open source makes it an obvious special case. There is at least one other open source Mac feed reader, and there are other open source Mac apps, and I don’t think that these projects are fueling the race to the bottom with app pricing.

\n\n

I went over and over this decision for months. It wasn’t easy! But in the end I decided it’s a good thing, and there are always good reasons not to do a good thing.

\n\n

* * *

\n\n\n

The app doesn’t have any icons yet. Brad Ellis, who I’ve worked with before on some versions of my previous feed reader, and who is my favorite designer, is working on icons.

\n\n

Brad is not only my favorite designer, he’s the favorite designer of people who thought they might be my favorite designer. :)

\n\n

* * *

\n\n\n

At some point it will sync with some existing systems (such as Feedly, FeedBin, and similar) — but probably not till after 1.0, though as top priority.

\n\n

I have no plans to make an iOS version (though anything could happen). The plan is to make it a great Mac app. Period. But if it syncs with Feedly and so on, then you could use some other reader on iOS and it would sync with Evergreen.

\n\n

* * *

\n\n\n

There is a road not taken here that’s worth exploring, though probably not by me (for reasons of time).

\n\n

I would love to see a casual feed reader (as opposed to productivity-style) that just provides a timeline, with new stuff at the top. The idea is to make something like a Twitter client but for feeds. You’d get a list of articles, and when you want to read something you’d click (or whatever) to open the article in your browser.

\n\n

Such an app wouldn’t have per-article read/unread status — instead it would maintain a high-water mark, the date of the newest item you’ve seen in the timeline.

\n\n

For a little while I was planning to do both styles of reader, since so much of the code would be shared. But that was overly ambitious, so I dropped the idea.

\n\n

But you could do it.

\n\n

* * *

\n\n\n

I made a Twitter account: evergreen_mac. Though I have no fondness for Twitter, it seems like app makers need to be accessible that way. Most Evergreen users will probably be on Twitter.

\n\n

But you don’t have to use it: you can report bugs and make feature requests via GitHub. And that way they’re in the system, which is good.

\n\n

You can also email me: I’m brent at the domain name that appears in the link that starts the first sentence of this post.

", + "date_published": "2017-06-01T17:27:32-07:00" + }, + { + "id": "http://inessential.com/2017/05/26/app_the_human_story_screening_in_san_", + "url": "http://inessential.com/2017/05/26/app_the_human_story_screening_in_san_", + "title": "“App: The Human Story” Screening in San Jose", + "content_html": "

Here’s the scoop. It’s Sunday, June 4 at 5 pm. There’s a panel afterward with a bunch of people from the movie (including me).

\n\n

You can get tickets. You should get tickets — the event benefits App Camp for Girls.

\n\n

Plus I think you’ll enjoy it. :)

", + "date_published": "2017-05-26T12:57:19-07:00" + }, + { + "id": "http://inessential.com/2017/05/17/json_feed", + "url": "http://inessential.com/2017/05/17/json_feed", + "title": "JSON Feed", + "content_html": "

I was hesitant, even up to this morning, to publish the JSON Feed spec.

\n\n

If you read Dave Winer’s Rules for standards-makers, you’ll see that we did a decent job with some of the rules — the spec is written in plain English, for example — but a strict application of the rules would have meant not publishing at all, since “Fewer formats is better.”

\n\n

I agree completely — but I also believe that developers (particularly Mac and iOS developers, the group I know best) are so loath to work with XML that they won’t even consider building software that needs an XML parser. Which says to me that JSON Feed is needed for the survival of syndication.

\n\n

I could be wrong, of course. I admit.

\n\n

Feed Reader Starter Kit

\n\n

See my RSXML repository for Objective-C code that reads RSS, Atom, and OPML. I’ve done the work for you of supporting those formats. Go write a feed reader! Seriously. Do it.

\n\n

I planned to have a JSON Feed parser for Swift done for today, but other things got in the way. It’s coming soon. But you probably don’t actually need any sample code, since JSON is so easy to handle.

\n\n

Feedback so far

\n\n

Feedback has been interesting so far. Some questions on the GitHub repo need answering.

\n\n

Some people have said this should have happened ten years ago, and other people have said that they hate how developers jump on the latest fad (JSON).

\n\n

And some people really like the icon:

\n\n

\n\n

Microformats

\n\n

One of the more serious criticisms was this: why not just support the hAtom microformat instead? Why do another side-file?

\n\n

My thinking:

\n\n

My experience as a feed reader author tells me that people screw up XML, badly, all the time — and they do even less well with HTML. So embedding info in HTML is just plain too difficult. In practice it would be even buggier than XML-based feeds.

\n\n

And there are other advantages to decoupling: a side-file can have 100 entries where there are only 10 on an HTML page, for instance. A side-file can have extra information that you wouldn’t put on an HTML page. And yet, despite the extra information, a side-file can be much smaller than an HTML page, and it can often be easier to cache (since it’s not different based on a logged-in user, for instance).

\n\n

Microformats sounds elegant, but I don’t prize elegance as much as I value things that work well.

", + "date_published": "2017-05-17T13:22:14-07:00" + }, + { + "id": "http://inessential.com/2017/05/01/frontier_diary_8_when_worlds_collide", + "url": "http://inessential.com/2017/05/01/frontier_diary_8_when_worlds_collide", + "title": "Frontier Diary #8: When Worlds Collide", + "content_html": "

I spent the weekend making a bunch of progress on the compiler. It has two pieces: a tokenizer, which I created by rewriting the original C code (langscan.c) in Swift, and a parser.

\n\n

The parser in OrigFrontier was generated by MacYacc, which is similar to Yacc, which is similar to Bison, which is on my Mac. The thing about the parser is that it’s C code, and the rest of the app is Swift.

\n\n

How do you bridge the two worlds? Easy answer: with Objective-C, which is a superset of C and which plays nicely (enough) with Swift.

\n\n

So I renamed langparser.y — the rules file that the parser generator uses — to langparser.ym so that Xcode would know to treat the generated parser source as Objective-C. I edited it slightly, not to change the grammar rules but to change how nodes are created (as return values rather than via inout).

\n\n

I also made my CodeTreeNode class, written in Swift, an Objective-C class so that it would be visible to my Objective-C code.

\n\n

And then, finally, I started a build…

\n\n

…and then it stopped with an error because the parser places my CodeTreeNode in a C union, which isn’t allowed in ARC.

\n\n

Crushed.

\n\n

* * *

\n\n\n

I think I have three options:

\n\n
    \n
  1. Go down the rabbit hole of figuring out how to get the parser to work with ARC.
  2. \n
  3. Go with the flow: have the parser generate nodes that are, as in OrigFrontier, C structs. The last compilation step would be Objective-C code that translates that tree of C structs into a tree of CodeTreeNode objects, and then disposes the C-struct-node-tree.
  4. \n
  5. Write the parser by hand, in Swift.
  6. \n
\n\n\n

My thinking:

\n\n

I could waste a ton of time on #1, and bending tools in that way can be pretty frustrating work when they refuse to bend.

\n\n

With #2 I’d feel a bit weird about the redundancy: building a tree and then building a copy of that tree with a different type of object.

\n\n

My heart tells me #3 is the answer. After all, I’ve already done the tokenizer. How hard would it be to parse those tokens into a code tree? I could skip C and Objective-C altogether and stay in Swift. And it would be so fun. (Because that’s precisely the style of weirdo I am.)

\n\n

* * *

\n\n\n

But the real answer is #2. Writing a parser by hand would take way longer than I think. Given enough tests, it shouldn’t be a huge source of bugs, but still.

\n\n

The thing about #2 is that yes, it’s redundant, it’s doing more work than it needs to, ideally — but my bet is that it would still be so fast that you wouldn’t be able to tell the difference. Computers are so good at this kind of thing. It’s not like reading files or networking; it’s just in-memory traversal and creating/releasing things.

\n\n

You remember in Indiana Jones that guy with the twirling swords, and Indy gives that look and then just shoots him? The second option is the Indiana Jones solution.

\n\n

Update 2:05 pm: Two people have already written me to recommend ANTLR. So I will definitely give that a look. It might be exactly what I need.

", + "date_published": "2017-05-01T13:34:23-07:00" + }, + { + "id": "http://inessential.com/2017/04/27/frontier_diary_7_pretty_much_everythin", + "url": "http://inessential.com/2017/04/27/frontier_diary_7_pretty_much_everythin", + "title": "Frontier Diary #7: Pretty Much Everything Throws", + "content_html": "

A script can throw an error, either intentionally (via the scriptError verb) or by doing something, such as referencing an undefined object, that generates an error.

\n\n

OrigFrontier was written in C, which has no error-throwing mechanism, and so it worked like this: most runtime functions returned a boolean (for success or failure), and the return value was passed in by reference. If there was an error, the function would set a global error variable and return false. The caller would then have to check that global to see if there was an error, and then do the right thing.

\n\n

This was not unreasonable, given the language and the times (early ’90s) and also given the need to be very careful about unwinding memory allocations.

\n\n

But, these days, it seems to me that Swift’s error system is the way to go. There’s just one downside to that, and it’s that I have to do that do/try/catch dance all over the place, since pretty much any runtime function can throw an error.

\n\n

Even the coercions can throw, so last night I changed the Value protocol so that asInt and so on are now functions, since properties can’t throw (at least not yet).

\n\n

The extra housekeeping — the do/try/catch stuff — kind of bugs me, but it’s honest. I considered making script errors just another type of Value — but that meant that all those callers have to check the returned Value to see if it’s an error, and then do the right thing. Better to just use Swift’s error system, because it makes for more consistent code, and it makes sure I’m catching errors in every case.

\n\n

It also means I’m not multiplying entities. A Swift error is a script error, and vice versa.

\n\n

* * *

\n\n\n

Working on this code is like applying the last 25 years of programming history all at once.

\n\n

A completely different type of error is a bug, and I’m certain to write a bunch of them, because that’s how programming goes.

\n\n

That’s where unit tests come in. Frontier has long had a stress-test suite of scripts — you’d launch the app, run that suite, wait a while, and see if there are any errors. This was critically helpful.

\n\n

But OrigFontier didn’t have unit tests at the C code level. The new version does. (Well, I’ve started them anyway.) This means I can more easily follow Rule 1 — the no-breakage rule — and can also more easily follow Rule 1b — the don’t-break-Dave rule.

\n\n

PS I’ve added a collection page for the Frontier Diary, as I did with earlier diaries. There’s a link to it in the footer of every page on the blog.

", + "date_published": "2017-04-27T13:30:42-07:00" + }, + { + "id": "http://inessential.com/2017/04/26/frontier_diary_6_ballard_from_the_par", + "url": "http://inessential.com/2017/04/26/frontier_diary_6_ballard_from_the_par", + "title": "Frontier Diary #6: Ballard, from the Parallel Universe", + "content_html": "

In another universe I didn’t decide to port Frontier — instead, I started over from scratch on an app inspired by Frontier.

\n\n

In that universe, the new scripting language, descended from UserTalk, is called Ballard. And it’s documented.

", + "date_published": "2017-04-26T13:04:04-07:00" + }, + { + "id": "http://inessential.com/2017/04/25/my_microblog", + "url": "http://inessential.com/2017/04/25/my_microblog", + "title": "My Microblog", + "content_html": "

I’m on Manton‘s cool new microblogs system. Here’s where you can follow me, once you’re on the system: http://micro.blog/brentsimmons.

\n\n

And here’s my microblog: http://brent.micro.blog/. (Which you can read using RSS, whether you’re on the system or not.)

\n\n

I wrote about three-quarters of my own single-user microblog system — and then stopped because I didn’t feel like running a server and because Manton’s service is so good.

", + "date_published": "2017-04-25T14:27:28-07:00" + }, + { + "id": "http://inessential.com/2017/04/25/frontier_diary_5_values_and_progress_o", + "url": "http://inessential.com/2017/04/25/frontier_diary_5_values_and_progress_o", + "title": "Frontier Diary #5: Values and Progress on the Language", + "content_html": "

I put the Frontier repository up on GitHub.

\n\n

(The build is currently broken. This is bad discipline, but since it’s still just me, I forgive myself. Sometimes I run out of time and I just commit what I have.)

\n\n

The repo has my new code and it also contains FrontierOrigFork, which is the original Frontier source with a bunch of deletions and some changes. The point is to give me 1) code to read and 2) a project that builds and runs on my 10.6.8 virtual machine.

\n\n

The original code is in C, and the port is, at least so far, all in Swift. In the end it should be almost all in Swift, but I anticipate a couple places where I may need to use Objective-C.

\n\n

Here’s one of the Swift wins:

\n\n

Values

\n\n

Since Frontier contains a database and scripting language, there’s a need for some kind of value object that could be a boolean, integer, string, date, and so on.

\n\n

Original Frontier used a tyvaluedata union, with fields for the various types of values.

\n\n

This is a perfectly reasonably approach in C. It’s great because you can pass the same type of value object everywhere.

\n\n

Were I writing this in Objective-C, however, I’d create a Value protocol, and then create new value objects for some types and also extend existing objects (NSNumber, NSString, etc.) to conform to the Value protocol. This would still give me the upside — passing a Value type everywhere — while reducing the amount of boxing.

\n\n

But: this still means I have an NSNumber when I really want a BOOL. Luckily, in Swift I can go one better: I can extend types such as Bool and Int to conform to a Value protocol.

\n\n

This means passing around an actual Bool rather than a boxed boolean. I like this a ton. It feels totally right.

\n\n

Other topic:

\n\n

Language Progress

\n\n

I’m still in architectural mode, where I’m writing just enough code to validate and refine my decisions. A couple days ago I started on the language evaluator — the thing that actually runs scripts.

\n\n

It works as you expect: it takes a compiled code tree and recursively evaluates it. It’s not difficult — it’s just that it’s going to end up being a fair amount of code.

\n\n

I’ve done just enough to know that I’m on the right path. (The Swift code looks a lot like the C code in OrigFrontier’s langevaluate.c. See evaluateList, for instance.)

\n\n

The next step is for me to build the parser. I thought about writing a parser by hand, because it sounds like fun, and it would give me some extra control — but, really, it would slow me way down, so forget it.

\n\n

OrigFrontier generated its parser by passing a grammar file — langparser.y — to MacYacc (there was such a thing!), which generated langparser.c.

\n\n

I’ll do a similar thing, except using Bison (which is compatible with Yacc). Or, possibly, using the Lemon parser generator instead. Either way, I’ll want the generated code to be Objective-C. (Well, mostly C, but with Objective-C objects instead of structs.) (I don’t know of a generator that would create Swift code.)

\n\n

This is completely new territory for me, and is exciting.

\n\n

(Almost forgot to mention: I’ll need to write a tokenizer. This means porting langscan.c. I’ll need to do this first, since the parser generator needs it. So this is the real next step.)

", + "date_published": "2017-04-25T13:26:33-07:00" + }, + { + "id": "http://inessential.com/2017/04/14/save_300_on_coccoaconf_next_door", + "url": "http://inessential.com/2017/04/14/save_300_on_coccoaconf_next_door", + "title": "Save $300 on CocoaConf Next Door", + "content_html": "

My pals at CocoaConf asked me to remind you that the Early Bird sale ends in two weeks for CocoaConf Next Door — the one taking place in San Jose during WWDC.

\n\n

I’ll be there. At least in the afternoons.

\n\n

Check out the speakers list. Yummy, chewy, nutty speakers list.

", + "date_published": "2017-04-14T13:53:02-07:00" + }, + { + "id": "http://inessential.com/2017/04/14/frontier_diary_4_the_quickdraw_problem", + "url": "http://inessential.com/2017/04/14/frontier_diary_4_the_quickdraw_problem", + "title": "Frontier Diary #4: The QuickDraw Problem and Where It Led Me", + "content_html": "

In my fork of Frontier there are still over 600 deprecation warnings. A whole bunch of these are due to QuickDraw calls.

\n\n

For those who don’t know: QuickDraw was how, in the old days, you drew things to the Mac’s screen. It was amazing for its time and pretty easy to work with. Functions included things like MoveTo, LineTo, DrawLine, FrameOval, and so on. All pretty straightforward.

\n\n

These days we have Core Graphics instead, and we have higher-level things like NSBezierPath. QuickDraw was simpler — though yes, sure, that was partly because it did less.

\n\n

* * *

\n\n\n

I was looking at all these deprecation warnings for QuickDraw functions and wondering how I’m ever going to get through them.

\n\n

I could, after all, convert all or most of them to the equivalent Core Graphics thing. But sheesh, what a bunch of work.

\n\n

And, in the end, it would still be a Carbon app, but with modern drawing.

\n\n

* * *

\n\n\n

So I thought about it from another angle. The goal is to get to the point where it’s a 64-bit Cocoa app. All these QuickDraw calls are in the service of UI — so why not just start over with a Cocoa UI?

\n\n

The app has some outlines (database browser, script editor, etc.), a basic text editor, and a handful of small dialogs. And all of that is super-easy in Cocoa.

\n\n

Use an NSOutlineView, NSTextView, and some xibs for the dialogs, and we’re done. (Well, after some work, but not nearly the same amount of work as actually writing an outliner from scratch.)

\n\n

In other words, instead of going from the bottom up — porting the existing source code — I decided to start from the top down.

\n\n

I started a new workspace and started a new Frontier project: a Cocoa app with Swift as the default language.

\n\n

Then I looked at the existing source and thought about how to organize things. I came up with this:

\n\n
    \n
  • Frontier — App UI
  • \n
  • UserTalk.framework — the language
  • \n
  • FrontierVerbs.framework - the standard library
  • \n
  • FrontierDB.framework — the object database
  • \n
  • FrontierCore.framework — common utility functions and extensions
  • \n
\n\n\n

I like using frameworks, because it helps enforce separation, and it helps in doing unit testing. And frameworks are so easy with Swift these days.

\n\n

Hardly any of this is filled-in yet. I’ve got the barest start on FrontierVerbs. Ted Howard, my partner in all this, is taking UserTalk.framework and FrontierDB.framework.

\n\n

In the end, it’s possible that no code from the original code base survives. Which is totally fine. But it also means that this is no quick project.

\n\n

At this point I should probably put it up on GitHub, since it’s easier to write about it if I can link to the code. I’ll do that soon, possibly on the weekend.

", + "date_published": "2017-04-14T13:14:20-07:00" + }, + { + "id": "http://inessential.com/2017/04/13/frontier_diary_3_built-in_verbs_config", + "url": "http://inessential.com/2017/04/13/frontier_diary_3_built-in_verbs_config", + "title": "Frontier Diary #3: Built-in Verbs Configuration", + "content_html": "

Frontier’s standard library is known as its built-in verbs. There are a number of different tables: file, clock, xml, and so on. Each contains a number of verbs: file.readWholeFile, clock.now, and so on.

\n\n

Most of these verbs are implemented in C, in the kernel, rather than as scripts. At the moment, to add one of these kernel verbs, you have to jump through a few hoops: edit a resource, add an integer ID, add to a switch statement, etc. It’s a pain and is error-prone.

\n\n

So I want to re-do this in Swift, because I’m all about Swift. And I want adding verbs to be fool-proof: I don’t want to remember how to configure this every single time I add a verb. Adding a verb needs to be easy.

\n\n

My thinking:

\n\n
    \n
  • Give each table its own class: ClockVerbs, FileVerbs, etc.
  • \n
  • Have each class report the names of the verbs it supports. These need to be strings, because we get a string at runtime.
  • \n
  • Run a verb simply by looking up the selector, performing it, and returning the result.
  • \n
\n\n\n

To make things easy and obvious, I think it should work like this: the selector for a given verb is its name plus a parameter. Then there’s not even a lookup step.

\n\n

Each verb will take a VerbParameters object and return a VerbResult object.

\n\n
dynamic func readWholeFile(_ params: VerbParameters) -> VerbResult\n
\n\n

The flow goes like this:

\n\n
    \n
  1. We have the string file.readWholeFile.
  2. \n
  3. We see the file suffix and so we know we need a FileVerbs object.
  4. \n
  5. We check fileVerbs.supportedVerbs (an array) to see if readWholeFile is in the list. It is.
  6. \n
  7. We construct a selector using the readWholeFile part of the string and we add a : character: NSSelectorFromString(verbName + \":\")
  8. \n
\n\n\n

This is great! We’re almost home free. Then we run the verb:

\n\n
if let result = perform(selector, with: params) as? VerbResult {\n    return result\n}\n
\n\n

That doesn’t work. We get:

\n\n
Cast from 'Unmanaged<AnyObject>! to unrelated type 'VerbResult' always fails\n
\n\n

Nuts.

\n\n

* * *

\n\n\n

It was so close.

\n\n

In Objective-C this would have worked. And obviously, apparently, I still think in Objective-C.

\n\n

I investigated some other options. At one point enums were abused, because there’s always, in Swift, an enum-abuse step. But everything I tried was more code and was more error-prone, and my goal here is to improve the situation.

\n\n

I think, in the end, I’m going to do something that looks kind of ugly: a switch statement where the cases are string literals.

\n\n
switch(verbName) {\ncase \"readWholeFile\":\n    return readWholeFile(params)\n…\n}\n
\n\n

“Nooooo!” you cry. I hear ya.

\n\n

My experience as an object-oriented programmer tells me this: if I write a switch statement, I blew it.

\n\n

And my experience as a programmer tells me that string literals are a bad idea.

\n\n

But the above may actually be the easiest to configure and maintain. Each string literal appears only in that one switch statement and nowhere else in the code. And the mapping between a verb name and its function couldn’t be more clear — it’s right there.

\n\n

(Yes, instead of using a string literal, I could create a String enum and switch on that. But that’s actually more code and more room for error. I’m going to have to type those string literals somewhere, so why not right where they’re used?)

\n\n

It does mean that readWholeFile appears three times in the code (the string literal, the call, and the function itself), and in an Objective-C version it would appear only twice (in a supportedVerbs array and the method itself).

\n\n

But. Well.

\n\n

I’m torn between shuddering in abject and complete horror at this solution and thinking, “Hey, that’s pretty straightforward. Anybody could read it. Anybody could edit it.” Which was the plan all along.

\n\n

And I get to stick with Swift, so there’s that.

\n\n

But, sure as shootin’, some day someone’s going to come across this code and say, “Brent, dude, are ya new?” And I’ll send them the link to this page.

\n\n

* * *

\n\n\n

Update the next day: well, the performSelector thing would work, if only I’d known about Swift Unmanaged objects.

\n\n

Joe Groff told me how this works.

\n\n

Here’s the gist: the Unmanaged<AnyObject> just needs to be unwrapped by calling takeRetainedValue or takeUnretainedValue. Once unwrapped, it can be cast to VerbResult.

\n\n

All this means that I can use my original design, which is great news.

\n\n

* * *

\n\n\n

Update April 25, 2017: I ended up using enums after all. See MathVerbs.swift for an example.

", + "date_published": "2017-04-13T22:25:41-07:00" + }, + { + "id": "http://inessential.com/2017/04/11/frontier_diary_2_two_good_ideas_that_a", + "url": "http://inessential.com/2017/04/11/frontier_diary_2_two_good_ideas_that_a", + "title": "Frontier Diary #2: Two Good Ideas that Aren’t Good Anymore", + "content_html": "

Strings in Frontier are usually either Pascal strings or Handles.

\n\n

You probably don’t know what I’m talking about. I’ll explain.

\n\n

Pascal Strings

\n\n

Frontier is a Mac Toolbox app that’s been Carbonized just enough to run on OS X. You may recall that the Mac Toolbox was written so long ago that the original API was in Pascal. That Pascal heritage lived on in many ways, even after everyone switched to C — and one of those ways was Pascal strings.

\n\n

A Pascal string is n bytes long, and the first byte specifies the length of the string, which leaves the rest of the bytes for the actual string. Str255 was probably most common, and certainly is most common in Frontier, but there are also smaller sizes: Str63 and Str31, for instance.

\n\n

Unlike C strings, they’re not zero-terminated, since there’s no need to calculate the length: you always know it from that first byte.

\n\n

You create a literal Pascal string like this…

\n\n
Str255 s = \"\\pThis is a string\";\n
\n\n

…and the compiler turns the \\p into the correct length (16 in this case).

\n\n

Now, I bet you’re saying to yourself, “Self, those Pascal strings are too small to be useful.”

\n\n

But consider this: every menu item name can fit into a Pascal string. You can fit a window title or a file name into a Pascal string (in fact, memory suggests that file names were even shorter, were Str31 Pascal strings). Any label or message on any bit of UI is probably short enough to fit into a Pascal string. (Especially if you assume English.)

\n\n

So for GUI apps these were terrifically useful, and the 255-byte limit was no problem. (You can fit a tweet in a Pascal string, after all, with a bunch of room left over. [Well, depending on the size of the characters.])

\n\n

Frontier still uses them internally a ton. (For some reason, in the Frontier code, Str255 strings are called bigstring, which sounds ironic, since they’re so small, but I think it was to differentiate them from even smaller Pascal strings such as Str31.)

\n\n

You might ask what the text encoding was for these strings.

\n\n

“Text whatzit?” I’d reply. “Oh, I see. Just regular.” (MacRoman.)

\n\n

It was a good idea, but its time has come and gone. We have better strings these days.

\n\n

Handles

\n\n

Frontier includes a scripting language and a database, which means it certainly has a need for strings much larger than 255 bytes.

\n\n

It also needs heap storage for other things — binary data, structs, etc. — that could be much larger than 255 bytes.

\n\n

Enter the Handle. A Handle points to a pointer that might move: the memory you access via a Handle is relocatable.

\n\n

Which sounds awful, I know, but it was a smart optimization in the days when your Mac’s memory would be a single-digit number of megabytes, or even less than that.

\n\n

Here’s the problem: your application’s heap space can become fragmented. It could have a whole bunch of gaps in it after a while. So, to regain that memory, the system could compact the heap — it would remove those gaps, which means relocating the memory pointed to via a Handle.

\n\n

This is better than running out of memory, obviously. But it means that you have to be careful when dereferencing a Handle: you have to actually lock it first — HLock(h) — so that it can’t be moved while you’re using it. (And then you unlock it — HUnlock(h) — when finished.)

\n\n

Handles are also resizable — SetHandleSize(h, size) — and resizing a Handle can result in it needing to move, if there’s not enough space where it is. Or other Handles might move. You don’t ever know, and don’t care, and you think this is elegant because the system handles it all for you.

\n\n

All you have to deal with is an additional level of indirection (**h instead of *p), locking and unlocking it when needed, and disposing of it — DisposeHandle(h) — when finished. (No, there’s no reference counting, slacker.)

\n\n

Nowadays, on OS X, Handles don’t ever move and there’s no heap compaction. So there’s no reason for them whatsoever. And they are, as expected, deprecated.

\n\n

Nevertheless, Frontier, a Mac Toolbox app written in C, uses Handles everywhere.

\n\n

(I remember being shocked, when I first started learning Cocoa 15 years ago, that there were no Handles. It seemed incredibly daring that objects were just pointers. It made me nervous!)

\n\n

The Size of the Job

\n\n

Almost all the Mac APIs that Frontier uses are deprecated. That’s one thing.

\n\n

But it’s worse than just that: the ways Frontier handles strings and pretty much every single thing it stores on the heap are also deprecated.

\n\n

So: what to do?

\n\n

The end goal is a Cocoa app, which means I’ll be able to use Foundation, CoreFoundation, and Swift data types: NSString and Swift String, for instance. There are a number of different structs in the code, and those will be turned into Objective-C and Swift objects and Swift structs.

\n\n

The tricky part, though, is getting from here to there. I think the first step is to start with Objective-C and Foundation types and use them where possible. I can do that without actually turning it into a Cocoa app (the app will still have its own WaitNextEvent event loop and Carbon windows) — which means I’ll have to bracket all Objective-C code in autorelease pools, and I’ll have to use manual retains and releases. I’m not sure how far that will get me, but it will get me closer.

\n\n

PS Here are a couple articles by Gwynne Raskind on the Mac Toolbox you might enjoy: Friday Q&A 2012-01-13: The Mac Toolbox and The Mac Toolbox: Followup.

", + "date_published": "2017-04-11T13:01:55-07:00" + }, + { + "id": "http://inessential.com/2017/04/05/two_little-known_and_completely_unrelate", + "url": "http://inessential.com/2017/04/05/two_little-known_and_completely_unrelate", + "title": "Two Little-Known and Completely Unrelated Facts", + "content_html": "

One. OmniOutliner’s outline view is implemented as CALayers rather than as a view with subviews. (I don’t think I’m giving away a trade secret here.)

\n\n

Two. If you eat fenugreek, your armpits will smell like maple syrup.

", + "date_published": "2017-04-05T16:57:59-07:00" + }, + { + "id": "http://inessential.com/2017/04/05/ios_javascript_and_object_hierarchies", + "url": "http://inessential.com/2017/04/05/ios_javascript_and_object_hierarchies", + "title": "iOS, JavaScript, and Object Hierarchies", + "content_html": "

Rob Fahrni:

\n\n

Given x-callback-url and App URL schemes in general it would be extremely cool to use those to create object hierarchies using JavaScript. Why JavaScript? Well, it’s native to iOS and applications can use the runtime.

", + "date_published": "2017-04-05T14:53:01-07:00" + }, + { + "id": "http://inessential.com/2017/04/05/cocoaconf_near_wwdc", + "url": "http://inessential.com/2017/04/05/cocoaconf_near_wwdc", + "title": "CocoaConf Near WWDC", + "content_html": "

There are a bunch of things happening near WWDC this year. Me, I’ll be at CocoaConf Next Door. I’m not preparing a talk, but I’ll probably be on a panel. And hanging out.

\n\n

Check out the speakers list, which includes Omni’s own Liz Marley. And a bunch of other people you totally want to see — Manton Reece, Jean MacDonald, Laura Savino, and plenty more.

\n\n

Also… AltConf and Layers will be near WWDC. If you could be in three places at once, you would. Well, four, including WWDC itself, I suppose. :)

", + "date_published": "2017-04-05T14:35:05-07:00" + }, + { + "id": "http://inessential.com/2017/04/05/omnioutliner_5_0_for_mac", + "url": "http://inessential.com/2017/04/05/omnioutliner_5_0_for_mac", + "title": "OmniOutliner 5.0 for Mac", + "content_html": "

I’ve been on the OmniOutliner team for over a year now. Though we don’t have positions like junior and senior developer, I enjoy calling myself the junior developer on the Outliner team, since I’m newest.

\n\n

I may be a new developer, but I’m not a new user — I’ve been using the app since the days when OmniOutliner 3 came installed on every Mac.

\n\n

Every time I start a talk, I outline it first. I organize the work I need to do in my side-project apps in OmniOutliner. And — don’t tell the OmniFocus guys, who are literally right here — sometimes I even use it for to-do management in general. I’d be lost without a great outliner.

\n\n

Anyway… there’s a new version: OmniOutliner 5.0. It’s my first dot-oh release at Omni, and I’m proud of it and proud of the team.

\n\n

As is common with our apps, we have two levels: a regular level and a Pro level. The regular level is called “Essentials” and is just $9.99. There’s a demo so you can try it out first.

\n\n

It syncs with iOS and with other Macs, by the way. Sync is free. And of course it comes with extensive documentation, and Omni’s awesome support humans are standing by.

\n\n

Get it while it’s hot!

", + "date_published": "2017-04-05T10:44:45-07:00" + }, + { + "id": "http://inessential.com/2017/04/03/frontier_diary_1_vm_life", + "url": "http://inessential.com/2017/04/03/frontier_diary_1_vm_life", + "title": "Frontier Diary #1: VM Life", + "content_html": "

It’s been years since I could build the Frontier kernel — but I finally got it building.

\n\n

It’s really a ’90s Mac app that’s been Carbonized just enough to run on MacOS, but it’s by no means modern: it uses QuickDraw and early Carbon APIs. It’s written entirely in C.

\n\n

I got it building by installing MacOS 10.6.8 Server in VMWare. Installed Xcode 3.2.6. And now, finally, I can build and run it.

\n\n

What is Frontier?

\n\n

Frontier — as some of you know — was a UserLand Software product in the ’90s and 2000s. I worked there for about six years.

\n\n

The app is a development environment and runtime: a persistent, hierarchical database with a scripting language and a GUI for browsing and editing the database and for writing, debugging, and running scripts.

\n\n

The Nerd’s Guide to Frontier gives some idea of what it’s like, though it was written before many of the later advances.

\n\n

Maybe you’ve never heard of it. But here’s the thing: it was in Frontier that the following were either invented or popularized and fleshed-out: scripted and templated websites, weblogs, hosted weblogs, web services over http, RSS, RSS readers, and OPML. (And things I’m forgetting.)

\n\n

Those innovations were due to the person — Dave Winer — and to the times, the relatively early web days. But they were also in part due to the tool: Frontier was a fantastic tool for implementing and iterating quickly.

\n\n

The Goal

\n\n

The high-level goal is to make that tool available again, because I think we need it.

\n\n

The plan is to turn it into a modern Mac app, a 64-bit Cocoa app, and then add new features that make sense these days. (There are so many!) But that first step is a big one.

\n\n

The first part of the first step is simple, and it’s where I am now: mass deletions of code. Every reference to THINK_C and MPWC has to go. All references to the 68K and PPC versions must go. There was a Windows port, and all that code is getting tossed. And then I’ll see the scale of what needs to be done.

\n\n

(Note: my repo is a fork, and it’s not even on the web yet. The code I’m deleting is never really gone.)

\n\n

I’m doing a blog diary on it because it helps keep me focused. Otherwise I’m jumping around on my side projects. But if I have to write about it, then I’ll stay on target.

", + "date_published": "2017-04-03T13:44:34-07:00" + }, + { + "id": "http://inessential.com/2017/03/31/the_goal", + "url": "http://inessential.com/2017/03/31/the_goal", + "title": "The Goal", + "content_html": "

The goal isn’t specifically impeachment and conviction. It’s for Trump to leave office.

\n\n

The stretch goal is that he dies broke and in prison.

\n\n

But we could settle for him going down in history as our worst President, as the worst person ever to become President, with the name Trump held in less esteem than that of Benedict Arnold, with Trumpism — that pseudo-populist white nationalism for the benefit of the super-rich — thoroughly loathed and seen for the brutish scam that it is.

\n\n

I think there comes a point before an actual trial in the Senate where Republican leaders — in Congress, in the Cabinet, wherever — realize that Trump can no longer govern, and they tell him so and urge him to resign.

\n\n

And I think he actually does resign at that point. He’s been through bankruptcy, and he’s shown that when there’s no path to winning, he’ll take the easiest route out of the situation, the route that leaves him the most status. He doesn’t have the stick-to-it-iveness to go to trial in the Senate: he’d quit.

\n\n

I don’t know what it will take to bring Republican leaders to this point. Their ongoing cowardice is the real scandal — when faced with a threat to our democracy, they play along because they’re hoping for some goodies.

\n\n

I don’t think they get to this point unless the public gets to this point, and so I look to the approval polls. If it gets below 30%, it’s probably there because of further revelations in the Russia affair, and it’s probably at the point where even cowards feel safe in doing the right thing — even if only to save their own necks, which will need saving.

\n\n

But right now Speaker Ryan won’t even replace Devin Nunes as chair of the house intelligence committee. So there’s still a long way to go.

", + "date_published": "2017-03-31T13:47:44-07:00" + }, + { + "id": "http://inessential.com/2017/03/25/my_cocoaconf_yosemite_2017_talk", + "url": "http://inessential.com/2017/03/25/my_cocoaconf_yosemite_2017_talk", + "title": "My CocoaConf Yosemite 2017 Talk", + "content_html": "

Yosemite 2017 was so great. It always is.

\n\n

Below is the rough draft of my first-night talk. A few notes…

\n\n

The actual spoken version is probably not even close to the text, which was written before any rehearsal, and of course it’s never my intent to memorize it exactly.

\n\n

The bit with Laura Savino was a quick three-chord rock medley. We both played acoustic guitar and sang. It went like this:

\n\n

B: Louie Louie, oh baby, we gotta go
\nL: Yeah yeah yeah yeah yeah
\nB: Louie Louie, oh baby, we gotta go
\nL: Yeah yeah yeah yeah yeah
\nB: I live on an apartment on the 99th floor of my block
\nL: Hang on Sloopy, Sloopy hang on
\nB: I look out my window imagining the world has stopped
\nL: Hang on Sloopy, Sloopy hang on
\n[Slight change of chords]
\nB & L: Teenage wasteland, oh yeah, only teenage wasteland [repeated]

\n\n

Here’s my favorite video for Brimful of Asha.

\n\n

During the Squirrel Picture interlude (slide #3) I told the Squirrel Story, which wasn’t planned or recently rehearsed, but I’ve told it often enough that it didn’t really need rehearsal.

\n\n

I dedicated the performance of Hallelujah to Dori Smith.

\n\n

The talk was meant to be about 20 minutes long. Afterward I went around the room with a microphone and each person introduced themselves. (The talk’s job is to be a first-night ice-breaker talk.)

\n\n

I spent about 10 hours on rehearsal for those 20 minutes.

\n\n

Here’s the talk:

\n\n

Slide #1: Three Chord Rock

\n\n

Hi. I’m Brent.

\n\n

Before I get started — seeing my friend Brad Ellis reminded me of the most rock-n-roll moment of my life. Where’s Brad? Hi Brad. Anyway — I was at a party at my friend Chris’s house, and he let me borrow his guitar and do a sing-along. I think we did White Rabbit and Me and Bobby McGee and Hotel California.

\n\n

Well, here’s the problem — I have a hard time hanging on to a guitar pick. Especially after a few beers. So at one point the pick goes flying, and I’m strumming with my fingers.

\n\n

But I had a hangnail, and it got a bit aggravated as I was strumming. At the end I noticed that there was my actual blood on the guitar. I felt bad about it, but Chris was gracious, of course, and I thought that right then: that’s rock and roll.

\n\n

You can use this as metaphor. Bleeding? Keep right on playing. Maybe you won’t even notice that you’re bleeding, at least not until you stop.

\n\n

Chris told me later that the guitar cleaned up fine, so all was well.

\n\n

Okay. On to the actual talk…

\n\n

I bet most of you have heard the phrase “three chord rock n roll.” Or have heard that “rock is so great because you only need three chords.”

\n\n

What you may not realize is that it’s even easier than that: it’s three specific chords. Always the same three chords.

\n\n

They might be in any key but they’re the first, fourth, and fifth. In the key of C, the first is C, the fourth is F, and the fifth is G. In the key of A it’s A, D, and E.

\n\n

And when a song does have more than those three chords, it has at least those three chords. They’re the foundation for almost all pop and rock.

\n\n

One part of music is building tension and then resolving it. I’ll demonstrate on guitar.

\n\n

[On guitar] Play the first .... and you’re fine. You’re home. Play the fourth .... and there’s a little tension. Not a ton, but some. But you want to go back to the first, to home.

\n\n

Then play the fifth ... and you have maximum tension. You definitely want to go back home to the first.

\n\n

So with those three chords you have everything you need to write a thousand songs.

\n\n

Now for a little demo, I’d like to invite Laura Savino up to help me out.

\n\n

[music]

\n\n

Thanks, Laura!

\n\n

SO LET ME MAKE TWO POINTS VERY CLEAR.

\n\n
    \n
  1. ONE. If you’re writing apps or a website or doing a podcast or whatever — if you’re just starting out and only know the equivalent of three chords, don’t worry — you can create a masterpiece with just three chords.

  2. \n
  3. TWO. If you do know more than three chords, you might want to consider just using those three chords anyway. People love those three chords. They’re appealing. They’re accessible and intimate. They work.

  4. \n
\n\n\n

Slide #2: “Brimful of Asha“ by Cornershop, Asha Bhosle, and You

\n\n

One of my personal favorite three-chord-rock songs came out in the mid-90s. Brimful of Asha by Cornershop.

\n\n

Who here knows this song?

\n\n

Let me explain what it’s about:

\n\n

Asha Bhosle sang songs for Bollywood musicals. The actresses would lip-sync, but it was her singing. She did this for over a thousand movies. Over 12,000 songs.

\n\n

Some of those songs would be released as singles. Years ago a single would come out on vinyl, as a 45. A 45 is smaller than a regular album, and it has one song on each side. The number 45 means 45 revolutions-per-minute — you’d have to set your turntable to 45 instead of the usual 33 1/3. So: a 45 is a single.

\n\n

So here’s a little bit from the song:

\n\n

[There’s dancing, behind movie screens…]

\n\n

I love that image. That Asha is not just singing but dancing as she’s singing. We never see her dancing, but that joy and engagement shows up in her performance.

\n\n

And so this song is about hope. It’s about how a song can bring some consolation and hope when people need it.

\n\n

And her name Asha actually means hope. Brimful of Asha — brimful of hope.

\n\n

HERE’S MY POINT.

\n\n

We're in the same business. People form an emotional connection to whatever we’re making. The things we make can bring hope to other people. Knowing that, it’s our job to be as engaged and joyful as she is as we make our things. Maybe we’re not literally dancing, but it should be the metaphorical equivalent.

\n\n

Slide #3: Squirrel Picture

\n\n

Squirrel!

\n\n

When I was a kid we went to a Methodist church. I haven’t been to church hardly at all since I was a kid, but I remember one cool thing from church services: the minister would pause and ask people to shake hands with the people around them.

\n\n

So here are the rules. Tell people to have a good conference, and shake hands with at least one person from another table. Stand up!

\n\n

Slide #4: “Hallelujah” by Leonard Cohen, with Singing by James Dempsey

\n\n

A few weeks ago I found myself in a hotel bar with a bunch of other nerds. I also found a piano. If there’s a piano, I’m going to play it. So I talked a few people — James Dempsey, Jean McDonald, Curt Clifton, and Jim Correia, into singing some songs.

\n\n

I forget who suggested Hallelujah. Might have been James. I didn’t know it very well, but I did my best. James sang, and he was awesome.

\n\n

So when I was thinking about this talk, I was thinking of doing the most beautiful possible thing I could do. So I remembered James singing this song.

\n\n

I may not be religious, but I think it’s plain that there is awesome magnificence greater than anything any human could make. It’s right outside.

\n\n

I’m not sure bears feel humble at the sight of these mountains; I’m not sure birds are awed at the vistas they fly over.

\n\n

But we do. Humans do. And knowing that we can’t measure up, it doesn’t stop us. Intead, we’re inspired.

\n\n

So here’s what I love about Hallelujah. It’s about trying and failing, and loving and losing — and singing Hallelujah anyway. In Cohen’s words, it may be a broken Hallelujah, but it’s still on our lips.

\n\n

James Dempsey please report to the stage.

\n\n

Everybody is encouraged to sing along. Especially to the chorus.

\n\n

[Hallelujah]

\n\n

Slide #5: Picture of my cat Papa

\n\n

I’m going to go around the room and have everyone introduce themselves. RULE: if anyone can’t hear, yell out.

", + "date_published": "2017-03-25T11:55:21-07:00" + } + ] +}