# Copyright (C) 2009, 2010 Roman Zimbelmann <romanz@lavabit.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
This module provides helper functions/classes for ranger.apps.
"""
import os, sys, re
from subprocess import Popen, PIPE
from ranger.ext.iter_tools import flatten
from ranger.shared import FileManagerAware
class Applications(FileManagerAware):
"""
This class contains definitions on how to run programs and should
be extended in ranger.apps
The user can decide what program to run, and if he uses eg. 'vim', the
function app_vim() will be called. However, usually the user
simply wants to "start" the file without specific instructions.
In such a case, app_default() is called, where you should examine
the context and decide which program to use.
All app functions have a name starting with app_ and return a string
containing the whole command or a tuple containing a list of the
arguments. They are supplied with one argument, which is the
AppContext instance.
You should define at least app_default, app_pager and app_editor since
internal functions depend on those. Here are sample implementations:
def app_default(self, context):
if context.file.media:
if context.file.video:
# detach videos from the filemanager
context.flags += 'd'
return self.app_mplayer(context)
else:
return self.app_editor(context)
def app_pager(self, context):
return ('less', ) + tuple(context)
def app_editor(self, context):
return ('vim', ) + tuple(context)
"""
def _meets_dependencies(self, fnc):
try:
deps = fnc.dependencies
except AttributeError:
return True
for dep in deps:
if hasattr(dep, 'dependencies') \
and not self._meets_dependencies(dep):
return False
if dep not in self.fm.executables:
return False
return True
def either(self, context, *args):
for app in args:
try:
application_handler = getattr(self, 'app_' + app)
except AttributeError:
if app in self.fm.executables:
return tup(app, *context)
continue
if self._meets_dependencies(application_handlerpre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */## the basic editor data structure, and how it displays text to the screen
# temporary main for this layer: just render the given string at the given
# screen dimensions, then stop
recipe! main [
local-scope
text:address:array:character <- next-ingredient
open-console
hide-screen 0/screen
new-editor text, 0/screen, 0/left, 5/right
show-screen 0/screen
wait-for-event 0/console
close-console
]
scenario editor-initially-prints-string-to-screen [
assume-screen 10/width, 5/height
run [
1:address:array:character <- new [abc]
new-editor 1:address:array:character, screen:address, 0/left, 10/right
]
screen-should-contain [
# top line of screen reserved for menu
. .
.abc .
. .
]
]
container editor-data [
# editable text: doubly linked list of characters (head contains a special sentinel)
data:address:duplex-list:character
top-of-screen:address:duplex-list:character
bottom-of-screen:address:duplex-list:character
# location before cursor inside data
before-cursor:address:duplex-list:character
# raw bounds of display area on screen
# always displays from row 1 (leaving row 0 for a menu) and at most until bottom of screen
left:number
right:number
# raw screen coordinates of cursor
cursor-row:number
cursor-column:number
]
# editor:address, screen <- new-editor s:address:array:character, screen:address, left:number, right:number
# creates a new editor widget and renders its initial appearance to screen.
# top/left/right constrain the screen area available to the new editor.
# right is exclusive.
recipe new-editor [
local-scope
s:address:array:character <- next-ingredient
screen:address <- next-ingredient
# no clipping of bounds
left:number <- next-ingredient
right:number <- next-ingredient
right <- subtract right, 1
result:address:editor-data <- new editor-data:type
# initialize screen-related fields
x:address:number <- get-address *result, left:offset
*x <- copy left
x <- get-address *result, right:offset
*x <- copy right
# initialize cursor
x <- get-address *result, cursor-row:offset
*x <- copy 1/top
x <- get-address *result, cursor-column:offset
*x <- copy left
init:address:address:duplex-list <- get-address *result, data:offset
*init <- push-duplex 167/§, 0/tail
top-of-screen:address:address:duplex-list <- get-address *result, top-of-screen:offset
*top-of-screen <- copy *init
y:address:address:duplex-list <- get-address *result, before-cursor:offset
*y <- copy *init
result <- insert-text result, s
# initialize cursor to top of screen
y <- get-address *result, before-cursor:offset
*y <- copy *init
# initial render to screen, just for some old tests
_, _, screen, result <- render screen, result
<editor-initialization>
reply result
]
recipe insert-text [
local-scope
editor:address:editor-data <- next-ingredient
text:address:array:character <- next-ingredient
# early exit if text is empty
reply-unless text, editor/same-as-ingredient:0
len:number <- length *text
reply-unless len, editor/same-as-ingredient:0
idx:number <- copy 0
# now we can start appending the rest, character by character
curr:address:duplex-list <- get *editor, data:offset
{
done?:boolean <- greater-or-equal idx, len
break-if done?
c:character <- index *text, idx
insert-duplex c, curr
# next iter
curr <- next-duplex curr
idx <- add idx, 1
loop
}
reply editor/same-as-ingredient:0
]
scenario editor-initializes-without-data [
assume-screen 5/width, 3/height
run [
1:address:editor-data <- new-editor 0/data, screen:address, 2/left, 5/right
2:editor-data <- copy *1:address:editor-data
]
memory-should-contain [
# 2 (data) <- just the § sentinel
# 3 (top of screen) <- the § sentinel
4 <- 0 # bottom-of-screen; null since text fits on screen
# 5 (before cursor) <- the § sentinel
6 <- 2 # left
7 <- 4 # right (inclusive)
8 <- 1 # cursor row
9 <- 2 # cursor column
]
screen-should-contain [
. .
. .
. .
]
]
# last-row:number, last-column:number, screen, editor <- render screen:address, editor:address:editor-data
#
# Assumes cursor should be at coordinates (cursor-row, cursor-column) and
# updates before-cursor to match. Might also move coordinates if they're
# outside text.
recipe render [
local-scope
screen:address <- next-ingredient
editor:address:editor-data <- next-ingredient
reply-unless editor, 1/top, 0/left, screen/same-as-ingredient:0, editor/same-as-ingredient:1
left:number <- get *editor, left:offset
screen-height:number <- screen-height screen
right:number <- get *editor, right:offset
# traversing editor
curr:address:duplex-list <- get *editor, top-of-screen:offset
prev:address:duplex-list <- copy curr # just in case curr becomes null and we can't compute prev-duplex
curr <- next-duplex curr
# traversing screen
+render-loop-initialization
color:number <- copy 7/white
row:number <- copy 1/top
column:number <- copy left
cursor-row:address:number <- get-address *editor, cursor-row:offset
cursor-column:address:number <- get-address *editor, cursor-column:offset
before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset
screen <- move-cursor screen, row, column
{
+next-character
break-unless curr
off-screen?:boolean <- greater-or-equal row, screen-height
break-if off-screen?
# update editor-data.before-cursor
# Doing so at the start of each iteration ensures it stays one step behind
# the current character.
{
at-cursor-row?:boolean <- equal row, *cursor-row
break-unless at-cursor-row?
at-cursor?:boolean <- equal column, *cursor-column
break-unless at-cursor?
*before-cursor <- copy prev
}
c:character <- get *curr, value:offset
<character-c-received>
{
# newline? move to left rather than 0
newline?:boolean <- equal c, 10/newline
break-unless newline?
# adjust cursor if necessary
{
at-cursor-row?:boolean <- equal row, *cursor-row
break-unless at-cursor-row?
left-of-cursor?:boolean <- lesser-than column, *cursor-column
break-unless left-of-cursor?
*cursor-column <- copy column
*before-cursor <- prev-duplex curr
}
# clear rest of line in this window
clear-line-delimited screen, column, right
# skip to next line
row <- add row, 1
column <- copy left
screen <- move-cursor screen, row, column
curr <- next-duplex curr
prev <- next-duplex prev
loop +next-character:label
}
{
# at right? wrap. even if there's only one more letter left; we need
# room for clicking on the cursor after it.
at-right?:boolean <- equal column, right
break-unless at-right?
# print wrap icon
print-character screen, 8617/loop-back-to-left, 245/grey
column <- copy left
row <- add row, 1
screen <- move-cursor screen, row, column
# don't increment curr
loop +next-character:label
}
print-character screen, c, color
curr <- next-duplex curr
prev <- next-duplex prev
column <- add column, 1
loop
}
# save first character off-screen
bottom-of-screen:address:address:duplex-list <- get-address *editor, bottom-of-screen:offset
*bottom-of-screen <- copy curr
# is cursor to the right of the last line? move to end
{
at-cursor-row?:boolean <- equal row, *cursor-row
cursor-outside-line?:boolean <- lesser-or-equal column, *cursor-column
before-cursor-on-same-line?:boolean <- and at-cursor-row?, cursor-outside-line?
above-cursor-row?:boolean <- lesser-than row, *cursor-row
before-cursor?:boolean <- or before-cursor-on-same-line?, above-cursor-row?
break-unless before-cursor?
*cursor-row <- copy row
*cursor-column <- copy column
*before-cursor <- copy prev
}
reply row, column, screen/same-as-ingredient:0, editor/same-as-ingredient:1
]
recipe clear-line-delimited [
local-scope
screen:address <- next-ingredient
column:number <- next-ingredient
right:number <- next-ingredient
{
done?:boolean <- greater-than column, right
break-if done?
print-character screen, 32/space
column <- add column, 1
loop
}
]
recipe clear-screen-from [
local-scope
screen:address <- next-ingredient
row:number <- next-ingredient
column:number <- next-ingredient
left:number <- next-ingredient
right:number <- next-ingredient
# if it's the real screen, use the optimized primitive
{
break-if screen
clear-display-from row, column, left, right
reply screen/same-as-ingredient:0
}
# if not, go the slower route
screen <- move-cursor screen, row, column
clear-line-delimited screen, column, right
clear-rest-of-screen screen, row, left, right
reply screen/same-as-ingredient:0
]
recipe clear-rest-of-screen [
local-scope
screen:address <- next-ingredient
row:number <- next-ingredient
left:number <- next-ingredient
right:number <- next-ingredient
row <- add row, 1
screen <- move-cursor screen, row, left
screen-height:number <- screen-height screen
{
at-bottom-of-screen?:boolean <- greater-or-equal row, screen-height
break-if at-bottom-of-screen?
screen <- move-cursor screen, row, left
clear-line-delimited screen, left, right
row <- add row, 1
loop
}
]
scenario editor-initially-prints-multiple-lines [
assume-screen 5/width, 5/height
run [
s:address:array:character <- new [abc
def]
new-editor s:address:array:character, screen:address, 0/left, 5/right
]
screen-should-contain [
. .
.abc .
.def .
. .
]
]
scenario editor-initially-handles-offsets [
assume-screen 5/width, 5/height
run [
s:address:array:character <- new [abc]
new-editor s:address:array:character, screen:address, 1/left, 5/right
]
screen-should-contain [
. .
. abc .
. .
]
]
scenario editor-initially-prints-multiple-lines-at-offset [
assume-screen 5/width, 5/height
run [
s:address:array:character <- new [abc
def]
new-editor s:address:array:character, screen:address, 1/left, 5/right
]
screen-should-contain [
. .
. abc .
. def .
. .
]
]
scenario editor-initially-wraps-long-lines [
assume-screen 5/width, 5/height
run [
s:address:array:character <- new [abc def]
new-editor s:address:array:character, screen:address, 0/left, 5/right
]
screen-should-contain [
. .
.abc ↩.
.def .
. .
]
screen-should-contain-in-color 245/grey [
. .
. ↩.
. .
. .
]
]
scenario editor-initially-wraps-barely-long-lines [
assume-screen 5/width, 5/height
run [
s:address:array:character <- new [abcde]
new-editor s:address:array:character, screen:address, 0/left, 5/right
]
# still wrap, even though the line would fit. We need room to click on the
# end of the line
screen-should-contain [
. .
.abcd↩.
.e .
. .
]
screen-should-contain-in-color 245/grey [
. .
. ↩.
. .
. .
]
]
scenario editor-initializes-empty-text [
assume-screen 5/width, 5/height
run [
1:address:array:character <- new []
2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right
3:number <- get *2:address:editor-data, cursor-row:offset
4:number <- get *2:address:editor-data, cursor-column:offset
]
screen-should-contain [
. .
. .
. .
]
memory-should-contain [
3 <- 1 # cursor row
4 <- 0 # cursor column
]
]
# just a little color for mu code
scenario render-colors-comments [
assume-screen 5/width, 5/height
run [
s:address:array:character <- new [abc
# de
f]
new-editor s:address:array:character, screen:address, 0/left, 5/right
]
screen-should-contain [
. .
.abc .
.# de .
.f .
. .
]
screen-should-contain-in-color 12/lightblue, [
. .
. .
.# de .
. .
. .
]
screen-should-contain-in-color 7/white, [
. .
.abc .
. .
.f .
. .
]
]
after <character-c-received> [
color <- get-color color, c
]
# color <- get-color color:number, c:character
# so far the previous color is all the information we need; that may change
recipe get-color [
local-scope
color:number <- next-ingredient
c:character <- next-ingredient
color-is-white?:boolean <- equal color, 7/white
# if color is white and next character is '#', switch color to blue
{
break-unless color-is-white?
starting-comment?:boolean <- equal c, 35/#
break-unless starting-comment?
trace 90, [app], [switch color back to blue]
color <- copy 12/lightblue
jump +exit:label
}
# if color is blue and next character is newline, switch color to white
{
color-is-blue?:boolean <- equal color, 12/lightblue
break-unless color-is-blue?
ending-comment?:boolean <- equal c, 10/newline
break-unless ending-comment?
trace 90, [app], [switch color back to white]
color <- copy 7/white
jump +exit:label
}
# if color is white (no comments) and next character is '<', switch color to red
{
break-unless color-is-white?
starting-assignment?:boolean <- equal c, 60/<
break-unless starting-assignment?
color <- copy 1/red
jump +exit:label
}
# if color is red and next character is space, switch color to white
{
color-is-red?:boolean <- equal color, 1/red
break-unless color-is-red?
ending-assignment?:boolean <- equal c, 32/space
break-unless ending-assignment?
color <- copy 7/white
jump +exit:label
}
# otherwise no change
+exit
reply color
]
scenario render-colors-assignment [
assume-screen 8/width, 5/height
run [
s:address:array:character <- new [abc
d <- e
f]
new-editor s:address:array:character, screen:address, 0/left, 8/right
]
screen-should-contain [
. .
.abc .
.d <- e .
.f .
. .
]
screen-should-contain-in-color 1/red, [
. .
. .
. <- .
. .
. .
]
]