Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
fxn committed Sep 10, 2012
0 parents commit a4408ef
Show file tree
Hide file tree
Showing 9 changed files with 702 additions and 0 deletions.
91 changes: 91 additions & 0 deletions README.md
@@ -0,0 +1,91 @@
# Terminal Keynote

![Terminal Keynote Cover](https://raw.github.com/fxn/tkn/master/screenshots/terminal-keynote-cover.png)

## Introduction

Terminal Keynote is a quick and dirty script I wrote for presenting my talks at [BaRuCo 2012](http://baruco.org) and [RailsClub 2012](http://railsclub.ru).

This is a total hack. It is procedural, uses a global variable, it has not been parametrized or generalized in anyway. It was tailor-made for what I exactly wanted but some people in the audience asked for the script. Even if it is quick and dirty I am very happy to share it so I have commented the source code and there you go!

## Markup

Fuck markup, this is text going to a terminal. If you want a list type "*"s. If you want bold face or colors use ANSI escape sequences.

Slides are written in Ruby. See the examples folder.

## Syntax Highlighting

Terminal Keynote is text-based, but with style! Syntax highlighting is done on the fly with @tmm1's [pygments.rb](https://github.com/tmm1/pygments.rb). The script uses the "terminal256" formatter and "bw" style, the lexer is also hard-coded to "ruby". Since this was tailor-made it has not been factored out.

## Master Slides

There are four types of slides:

### :code

A slide with source code. Syntax highlighted on the fly. If you want to put a title or file name or something use source code comments and imagination.

![Terminal Keynote Code](https://raw.github.com/fxn/tkn/master/screenshots/terminal-keynote-code.png)

### :center

A slide whose text is centered line by line.

![Terminal Keynote Center](https://raw.github.com/fxn/tkn/master/screenshots/terminal-keynote-center.png)

### :block

A slide with text content whose formatting is preserved, but that is centered as a whole in the screen. Do that with CSS, ha!

I find centering content in the screen as a block to be more aesthetically pleasant that flushing against the left margin. There is not way to flush against a margin.

![Terminal Keynote Block](https://raw.github.com/fxn/tkn/master/screenshots/terminal-keynote-block.png)

### Sections

Sections have a title and draw kind of a fleuron. This is also hard-coded because it is what I wanted.

Sections allow you to group slides in your Ruby slide deck, and since they yield to a block you can collapse/fold the ones you are not working in for focus.

The nested structure is not modelled internally. The script only sees a flat linear sequence of slides.

![Terminal Keynote Section](https://raw.github.com/fxn/tkn/master/screenshots/terminal-keynote-section.png)

## Visual Effects

There is a hard-coded visual effect: Once the exact characters of a given slide are computed, we print char by char with a couple milliseconds in between. That gives the illusion of an old-school running cursor effect. Configure block blinking cursor for maximum awesomeness.

## Installation

By now this is not going to be a gem, please clone the repo and hack your talk. In its current state it is just too tailor-made for anything but personal forks. Please keep the script together with the slides, that way you guarantee one year later the presentation will still run.

If Terminal Keynote evolves it is going to do so in backwards incompatible ways for sure. So, let's wait. If the thing ever converges to something that can be packaged then I'll do it.

## Keyboard Controls and Remotes

* To go forward press any of " ", "n", "k", "l", PageDown (but see below).

* To go backwards press any of "b", "p", "h", "j", PageUp (but see below).

* Beginning of the talk: "^"

* End of the talk: "$"

My Logitech remote emits PageDown and PageUp. You get those as ANSI escape sequences "\e[5~" and "\e[6~" respectively. The script understands them, but you need to [configure them in Terminal.app](http://fplanque.com/dev/mac/mac-osx-terminal-page-up-down-home-end-of-line) and also tell it to pass them down to the shell selecting "send string to the shell" in the "Action" selector.

## Font and Terminal Configuration

I used Menlo, 32 points. That gives 18x52 in a screen resolution of 1024x768.

For your setup: Find out the resolution of the projector of your conference (ask the organization in advance). Set the screen to that resolution, choose font size and maximize window. When you like how it looks, then run `stty size` and write down rows and cols.

Then, define in your terminal application a profile for the theme you like, and initial dimensions to those rows and cols. That way the terminal will launch with those dimensions no matter the screen resolution and you can hack your talk in your day to day with the native resolution, knowing how is going to look in proportion.

## Editor Snippets

A snippet for your editor is basic to write slides quickly. The extras folder has a snippet for Sublime Text 2.

## Cathode

[Cathode](http://www.secretgeometry.com/apps/cathode/) is perfect for this thing. But because of how it draws the text it doesn't do bold faces and may not be able to render some colors or Unicode characters. YMMV.
178 changes: 178 additions & 0 deletions bin/tkn
@@ -0,0 +1,178 @@
#!/usr/bin/env ruby
# encoding: utf-8

# UTF-8 ALL THE THINGS.
Encoding.default_external = 'utf-8'
Encoding.default_internal = 'utf-8'

require 'active_support/core_ext/string/strip'
require 'pygments'


#
# --- DSL -------------------------------------------------------------
#

def slide(content, format=:block)
$slides << [content.strip_heredoc, format]
end

def section(content)
$slides << [content, :section]
yield
end


#
# --- ANSI Escape Sequences -------------------------------------------
#

# Clears the screen and leaves the cursor at the top left corner.
def clear_screen
"\e[2J\e[H"
end

# Puts the cursor at (row, col), 1-based.
#
# Note that characters start to get printed where the cursor is. So, to leave
# a left margin of 8 characters you want col to be 9.
def cursor_at(row, col)
"\e[#{row};#{col}H"
end


#
# --- Utilities -------------------------------------------------------
#

# Returns the width of the content, defined as the maximum length of its lines
# discarding trailing newlines if present.
def width(content)
content.each_line.map do |line|
ansi_length(line.chomp)
end.max
end

# Quick hack to compute the length of a string ignoring the characters that
# represent ANSI escape sequences. This only supports a handful of them, the
# ones that I want to use.
def ansi_length(str)
str.gsub(/\e\[(2J|\d*(;\d+)*(m|f|H))/, '').length
end

# Returns the number of rows and columns of the terminal as an array of two
# integers [rows, cols]. We could use io/console here but shelling out is also
# fine.
def winsize
`stty size`.split.map(&:to_i)
end


#
# --- Slide Rendering -------------------------------------------------
#

# Returns a string that the caller has to print as is to get the slide
# properly rendered. The caller is responsible for clearing the screen.
def render(slide)
send("render_#{slide[1]}", slide[0]) if slide[0] =~ /\S/
end

# Renders the content by centering each individual line.
def render_center(content)
nrows, ncols = winsize

''.tap do |str|
nlines = content.count("\n")
row = [1, 1 + (nrows - nlines)/2].max
content.each_line.with_index do |line, i|
col = [1, 1 + (ncols - ansi_length(line.chomp))/2].max
str << cursor_at(row + i, col) + line
end
end
end

# Renders a section banner.
def render_section(content)
nrows, ncols = winsize
width = width(content)

rfil = [1, width - 5].max/2
lfil = [1, width - 5 - rfil].max
fleuron = '─' * lfil + ' ❧❦☙ ' + '─' * rfil

render_center("#{fleuron}\n\n#{content}\n\n#{fleuron}\n")
end

# Renders Ruby source code.
def render_code(code)
render_block(Pygments.highlight(code, formatter: 'terminal256', lexer: 'ruby', options: {style: 'bw'}))
end

# Centers the whole content as a block. That is, the format within the content
# is preserved, but the whole thing looks centered in the terminal. I think
# this looks nicer than an ordinary flush against the left margin.
def render_block(content)
nrows, ncols = winsize

nlines = content.count("\n")
row ||= [1, 1 + (nrows - nlines)/2].max

width = width(content)
col ||= [1, 1 + (ncols - width)/2].max

content.gsub(/^/) do
cursor_at(row, col).tap { row += 1 }
end
end


#
# --- Main Loop -------------------------------------------------------
#

# Reads either one single character or PageDown or PageUp. You need to
# configure Terminal.app so that PageDown and PageUp get passed down the
# script. Echoing is turned off while doing this.
def read_command
begin
system 'stty raw -echo'
command = STDIN.getc
# Consume PageUp or PageDown if present. No other ANSI escape sequence is
# supported so a blind 3.times getc is enough.
3.times { command << STDIN.getc } if command == "\e"
command
ensure
system "stty -raw echo"
end
end

n = 0
loop do
print clear_screen

# We load the presentation in every iteration to ease editing and reload.
# This is fast enough, so who cares about caching.
$slides = []
load ARGV[0]

n = [[0, n].max, $slides.length - 1].min
render($slides[n]).each_char do |c|
print c
sleep 0.002 # old-school touch: running cursor
end

case read_command
when ' ', 'n', 'l', 'k', "\e[5~"
n += 1
when 'b', 'p', 'h', 'j', "\e[6~"
n -= 1
when '^'
n = 0
when '$'
n = $slides.length - 1
when 'q'
print clear_screen
exit
end
end

0 comments on commit a4408ef

Please sign in to comment.