summary refs log blame commit diff stats
path: root/ranger/actions.py
blob: d35a2c489436633d344f63d9caf416425476351d (plain) (tree)
1
2
3
4
5
6
7
8
9
10


             
                                                         
                           
                                                      
                                                    
 

                                   
                                               
                               


                                                   
                                      























                                                                                

                                                            
                                                      

                                                     
                                                                     





                                                                                                 
                                                              


                                                         
                                                         
                                                                           

                                                     

                            



                                                                               







                                          
                                               
                                     

                       
                                      


                                  
                                                           
                                               
 












                                                              


                                                          














                                                              


                                                          
                                      
                                                            

                                                         

                                                        
                                                         
                                                            
                                


                                    
                                                                                   
                                                  

                                      
                                                             

                                          
                                 
                                                




                                                               
        
                                     
                                                                             
                                


                                                        
                                              
                               

                                                                             

                                       
                                                        


                                             
                                                             
                                      
        



















                                                                                             

                                                                   
        




                                                      



                                                                           
        










                                                               
 
                                            
                                  



                                                                               
 


                                                      

                                       



                                                            

                            
                                                                               



                                                              
                                                    
                                                                      
                                                    
                                                          

                                                              
                                                                          
                                                     

                                                  
                                                               
                                                                                
 





                                                                            


                                                                
 
                                   
                                                     
                                              
                                                
                                                              
 
                                
                                       
                                       

                        
                                                                          




                                                
                                                            

                                                               






                                                                    
        



                                            
 




                                                  

                                                      


                                                                        




































                                                                         
 

                                                          

                                                         
 
                                                                    



                                             
                                                   









                                                                                   

                                                                       
                                                   
 


                                    





                                                  
                                

                                             








                                                                                 
                     















                                                                                                     

                         
                                                    
                                                   



                                                         



                                                                               


                                                                 

                                                                               

                              



                                                                       










                                                       
                                                      
 
                      
import os
import shutil

from ranger.shared import EnvironmentAware, SettingsAware
from ranger import fsobject
from ranger.ext.trim import trimmed_lines_of_docstring
from ranger.gui.widgets import console_mode as cmode

from ranger.applications import run

class Actions(EnvironmentAware, SettingsAware):
	search_method = 'ctime'
	search_forward = False

	def search(self, order=None, forward=True):
		original_order = order
		if self.search_forward:
			direction = bool(forward)
		else:
			direction = not bool(forward)

		if order is None:
			order = self.search_method
		else:
			self.set_search_method(order=order)

		if order in ('search', 'tag'):
			if order == 'search':
				arg = self.env.last_search
				if arg is None:
					return False
				if hasattr(arg, 'search'):
					fnc = lambda x: arg.search(x.basename)
				else:
					fnc = lambda x: arg in x.basename
			elif order == 'tag':
				fnc = lambda x: x.realpath in self.tags

			return self.env.pwd.search_fnc(fnc=fnc, forward=forward)

		elif order in ('size', 'mimetype', 'ctime'):
			pwd = self.env.pwd
			if original_order is not None:
				lst = list(pwd.files)
				if order == 'size':
					fnc = lambda item: -item.size
				elif order == 'mimetype':
					fnc = lambda item: item.mimetype
				elif order == 'ctime':
					fnc = lambda item: -int(item.stat and item.stat.st_ctime)
				lst.sort(key=fnc)
				pwd.set_cycle_list(lst)
				return pwd.cycle(forward=None)

			return pwd.cycle(forward=forward)

	def set_search_method(self, order, forward=True):
		if order in ('search', 'tag', 'size', 'mimetype', 'ctime'):
			self.search_method = order
			self.search_forward = forward

	def interrupt(self):
		"""
		Waits a short time.
		If CTRL+C is pressed while waiting, the program will be exited.
		"""
		import time
		self.env.key_clear()
		try:
			time.sleep(0.2)
		except KeyboardInterrupt:
			raise SystemExit()

	def resize(self):
		"""Update the size of the UI"""
		self.ui.update_size()

	def exit(self):
		"""Exit the program"""
		raise SystemExit()

	def enter_dir(self, path):
		"""Enter the directory at the given path"""
		return self.env.enter_dir(path)

	def tag_toggle(self, movedown=None):
		try:
			toggle = self.tags.toggle
		except AttributeError:
			return

		sel = self.env.get_selection()
		toggle(*tuple(map(lambda x: x.realpath, sel)))

		if movedown is None:
			movedown = len(sel) == 1
		if movedown:
			self.move_pointer(relative=1)

		if hasattr(self.ui, 'redraw_main_column'):
			self.ui.redraw_main_column()
	
	def tag_remove(self, movedown=None):
		try:
			remove = self.tags.remove
		except AttributeError:
			return

		sel = self.env.get_selection()
		remove(*tuple(map(lambda x: x.realpath, sel)))

		if movedown is None:
			movedown = len(sel) == 1
		if movedown:
			self.move_pointer(relative=1)

		if hasattr(self.ui, 'redraw_main_column'):
			self.ui.redraw_main_column()

	def enter_bookmark(self, key):
		"""Enter the bookmark with the name <key>"""
		try:
			destination = self.bookmarks[key]
			pwd = self.env.pwd
			if destination.path != pwd.path:
				self.bookmarks.enter(key)
				self.bookmarks.remember(pwd)
		except KeyError:
			pass

	def set_bookmark(self, key):
		"""Set the bookmark with the name <key> to the current directory"""
		self.bookmarks[key] = self.env.pwd

	def unset_bookmark(self, key):
		"""Delete the bookmark with the name <key>"""
		self.bookmarks.delete(key)

	def move_left(self, n=1):
		"""Enter the parent directory"""
		try:
			directory = os.path.join(*(['..'] * n))
		except:
			return
		self.env.enter_dir(directory)
	
	def move_right(self, mode=0):
		"""Enter the current directory or execute the current file"""
		cf = self.env.cf
		marked_items = self.env.pwd.marked_items
		sel = self.env.get_selection()

		if not self.env.enter_dir(cf):
			if sel:
				if self.execute_file(sel, mode=mode) is None:
					self.open_console(cmode.OPEN_QUICK)

	def history_go(self, relative):
		"""Move back and forth in the history"""
		self.env.history_go(relative)
	
	def handle_mouse(self):
		"""Handle mouse-buttons if one was pressed"""
		self.ui.handle_mouse()
	
	def display_command_help(self, console_widget):
		if not hasattr(self.ui, 'open_pager'):
			return

		try:
			command = console_widget._get_cmd_class()
		except:
			self.notify("Feature not available!", bad=True)
			return

		if not command:
			self.notify("Command not found!", bad=True)
			return

		if not command.__doc__:
			self.notify("Command has no docstring. Try using python without -OO",
					bad=True)
			return

		pager = self.ui.open_pager()
		lines = trimmed_lines_of_docstring(command.__doc__)
		pager.set_source(lines)
	
	def display_log(self):
		if not hasattr(self.ui, 'open_pager'):
			return

		pager = self.ui.open_pager()
		if self.log:
			pager.set_source(["Message Log:"] + list(self.log))
		else:
			pager.set_source(["Message Log:", "No messages!"])
	
	def display_file(self):
		if not hasattr(self.ui, 'open_embedded_pager'):
			return

		try:
			f = open(self.env.cf.path, 'r')
		except:
			pass
		else:
			pager = self.ui.open_embedded_pager()
			pager.set_source(f)

	def execute_file(self, files, **kw):
		"""Execute a file.
		app is the name of a method in Applications, without the "app_"
		flags is a string consisting of applications.ALLOWED_FLAGS
		mode is a positive integer.
		Both flags and mode specify how the program is run."""

		if isinstance(files, set):
			files = list(files)
		elif type(files) not in (list, tuple):
			files = [files]

		return run(fm=self, files=list(files), **kw)

	def execute_command(self, cmd, **kw):
		return run(fm=self, action=cmd, **kw)
	
	def edit_file(self):
		"""Calls execute_file with the current file and app='editor'"""
		if self.env.cf is None:
			return
		self.execute_file(self.env.cf, app = 'editor')

	def open_console(self, mode=':', string=''):
		"""Open the console if the current UI supports that"""
		if hasattr(self.ui, 'open_console'):
			self.ui.open_console(mode, string)

	def move_pointer(self, relative = 0, absolute = None):
		"""Move the pointer down by <relative> or to <absolute>"""
		self.env.pwd.move(relative, absolute)

	def move_pointer_by_pages(self, relative):
		"""Move the pointer down by <relative> pages"""
		self.env.pwd.move(relative=int(relative * self.env.termsize[0]))

	def move_pointer_by_percentage(self, relative=0, absolute=None):
		"""Move the pointer down by <relative>% or to <absolute>%"""
		try:
			factor = len(self.env.pwd) / 100.0
		except:
			return
		self.env.pwd.move(
				relative=int(relative * factor),
				absolute=int(absolute * factor))

	def scroll(self, relative):
		"""Scroll down by <relative> lines"""
		if hasattr(self.ui, 'scroll'):
			self.ui.scroll(relative)
			self.env.cf = self.env.pwd.pointed_obj

	def redraw_window(self):
		"""Redraw the window"""
		self.ui.redraw_window()

	def reset(self):
		"""Reset the filemanager, clearing the directory buffer"""
		old_path = self.env.pwd.path
		self.env.directories = {}
		self.enter_dir(old_path)

	def toggle_boolean_option(self, string):
		"""Toggle a boolean option named <string>"""
		if isinstance(self.env.settings[string], bool):
			self.env.settings[string] ^= True

	def sort(self, func=None, reverse=None):
		if reverse is not None:
			self.env.settings['reverse'] = bool(reverse)

		if func is not None:
			self.env.settings['sort'] = str(func)
	
	def force_load_preview(self):
		cf = self.env.cf
		if cf is not None:
			cf.force_load = True

	def set_filter(self, fltr):
		try:
			self.env.pwd.filter = fltr
		except:
			pass
	
	def notify(self, text, duration=4, bad=False):
		self.log.appendleft(text)
		if hasattr(self.ui, 'notify'):
			self.ui.notify(text, duration=duration, bad=bad)
	
	def mark(self, all=False, toggle=False, val=None, movedown=None):
		"""
		A wrapper for the directory.mark_xyz functions.

		Arguments:
		all - change all files of the current directory at once?
		toggle - toggle the marked-status?
		val - mark or unmark?
		"""

		if self.env.pwd is None:
			return

		pwd = self.env.pwd

		if movedown is None:
			movedown = not all

		if val is None and toggle is False:
			return

		if all:
			if toggle:
				pwd.toggle_all_marks()
			else:
				pwd.mark_all(val)
		else:
			item = self.env.cf
			if item is not None:
				if toggle:
					pwd.toggle_mark(item)
				else:
					pwd.mark_item(item, val)

		if movedown:
			self.move_pointer(relative=1)

		if hasattr(self.ui, 'redraw_main_column'):
			self.ui.redraw_main_column()
		if hasattr(self.ui, 'status'):
			self.ui.status.need_redraw = True

	# ------------------------------------ filesystem operations

	def copy(self):
		"""Copy the selected items"""

		selected = self.env.get_selection()
		self.env.copy = set(f for f in selected if f in self.env.pwd.files)
		self.env.cut = False
	
	def cut(self):
		self.copy()
		self.env.cut = True

	def paste(self):
		"""Paste the selected items into the current directory"""
		from os.path import join, isdir
		from ranger.ext import shutil_generatorized as shutil_g
		from ranger.fsobject.loader import LoadableObject
		copied_files = tuple(self.env.copy)

		if not copied_files:
			return

		pwd = self.env.pwd
		try:
			one_file = copied_files[0]
		except:
			one_file = None

		if self.env.cut:
			self.env.copy.clear()
			self.env.cut = False
			if len(copied_files) == 1:
				descr = "moving: " + one_file.path
			else:
				descr = "moving files from: " + one_file.dirname
			def generate():
				for f in copied_files:
					for _ in shutil_g.move(f.path, pwd.path):
						yield
				pwd.load_content()
		else:
			if len(copied_files) == 1:
				descr = "copying: " + one_file.path
			else:
				descr = "copying files from: " + one_file.dirname
			def generate():
				for f in self.env.copy:
					if isdir(f.path):
						for _ in shutil_g.copytree(f.path,
								join(self.env.pwd.path, f.basename)):
							yield
					else:
						for _ in shutil_g.copy2(f.path, self.env.pwd.path):
							yield
				pwd.load_content()

		self.loader.add(LoadableObject(generate(), descr))

	def delete(self):
		self.notify("Deleting!", duration=1)
		selected = self.env.get_selection()
		self.env.copy -= selected
		if selected:
			for f in selected:
				if os.path.isdir(f.path):
					try:
						shutil.rmtree(f.path)
					except OSError as err:
						self.notify(str(err), bad=True)
				else:
					try:
						os.remove(f.path)
					except OSError as err:
						self.notify(str(err), bad=True)
	
	def mkdir(self, name):
		try:
			os.mkdir(os.path.join(self.env.pwd.path, name))
		except OSError as err:
			self.notify(str(err), bad=True)

	
	def rename(self, src, dest):
		if hasattr(src, 'path'):
			src = src.path

		try:
			os.rename(src, dest)
		except OSError as err:
			self.notify(str(err), bad=True)

	# ------------------------------------ aliases

	cd = enter_dir
inenos .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 */
//: A debugging helper that lets you zoom in/out on a trace.
//: Warning: this tool has zero automated tests.
//:
//: To try it out, first create an example trace:
//:   mu --trace nqueens.mu
//: Then to browse the trace, which was stored in a file called 'last_run':
//:   mu browse-trace last_run
//:
//: You should now find yourself in a UI showing a subsequence of lines from
//: the trace, each line starting with a numeric depth, and ending with a
//: parenthetical count of trace lines hidden after it with greater depths.
//:
//: For example, this line:
//:   2 app: line1 (30)
//: indicates that it was logged with depth 2, and that 30 following lines
//: have been hidden at a depth greater than 2.
//:
//: As an experiment, hidden counts of 1000 or more are in red to highlight
//: where you might be particularly interested in expanding.
//:
//: The UI provides the following hotkeys:
//:
//:   `q` or `ctrl-c`: Quit.
//:
//:   `Enter`: 'Zoom into' this line. Expand some or all of the hidden lines
//:   at the next higher level, updating parenthetical counts of hidden lines.
//:
//:   `Backspace`: 'Zoom out' on a line after zooming in, collapsing expanded
//:   lines below by some series of <Enter> commands.
//:
//:   `j` or `down-arrow`: Move/scroll cursor down one line.
//:   `k` or `up-arrow`: Move/scroll cursor up one line.
//:   `J` or `ctrl-f` or `page-down`: Scroll cursor down one page.
//:   `K` or `ctrl-b` or `page-up`: Scroll cursor up one page.
//:   `h` or `left-arrow`: Scroll cursor left one character.
//:   `l` or `right-arrow`: Scroll cursor right one character.
//:   `H`: Scroll cursor left one screen-width.
//:   `L`: Scroll cursor right one screen-width.
//:
//:   `g` or `home`: Move cursor to start of trace.
//:   `G` or `end`: Move cursor to end of trace.
//:
//:   `t`: Move cursor to top line on screen.
//:   `c`: Move cursor to center line on screen.
//:   `b`: Move cursor to bottom line on screen.
//:   `T`: Scroll line at cursor to top of screen.
//:
//:   `/`: Search forward for a pattern.
//:   '?': Search backward for a pattern.
//:   `n`: Repeat the previous '/' or '?'.
//:   `N`: Repeat the previous '/' or '?' in the opposite direction.
//:
//:   After hitting `/`, a small editor on the bottom-most line supports the
//:   following hotkeys:
//:     ascii characters: add the key to the pattern.
//:     `Enter`: search for the pattern.
//:     `Esc` or `ctrl-c`: cancel the current search, setting the screen back
//:       to its state before the search.
//:     `left-arrow`: move cursor left.
//:     `right-arrow`: move cursor right.
//:     `ctrl-a` or `home`: move cursor to start of search pattern.
//:     `ctrl-e` or `end`: move cursor to end of search pattern.
//:     `ctrl-u`: clear search pattern before cursor
//:     `ctrl-k`: clear search pattern at and after cursor

:(before "End Primitive Recipe Declarations")
_BROWSE_TRACE,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "$browse-trace", _BROWSE_TRACE);
:(before "End Primitive Recipe Checks")
case _BROWSE_TRACE: {
  break;
}
:(before "End Primitive Recipe Implementations")
case _BROWSE_TRACE: {
  start_trace_browser();
  break;
}

//: browse a trace loaded from a file
:(after "Commandline Parsing")
if (argc == 3 && is_equal(argv[1], "browse-trace")) {
  load_trace(argv[2]);
  start_trace_browser();
  return 0;
}

:(before "End Globals")
set<int> Visible;
int Top_of_screen = 0;
int Left_of_screen = 0;
int Last_printed_row = 0;
map<int, int> Trace_index;  // screen row -> trace index
string Current_search_pattern = "";
:(before "End Types")
enum search_direction { FORWARD, BACKWARD };
:(before "End Globals")
search_direction Current_search_direction = FORWARD;

:(code)
void start_trace_browser() {
  if (!Trace_stream) return;
  cerr << "computing min depth to display\n";
  int min_depth = 9999;
  for (int i = 0;  i < SIZE(Trace_stream->past_lines);  ++i) {
    trace_line& curr_line = Trace_stream->past_lines.at(i);
    if (curr_line.depth < min_depth) min_depth = curr_line.depth;
  }
  cerr << "min depth is " << min_depth << '\n';
  cerr << "computing lines to display\n";
  for (int i = 0;  i < SIZE(Trace_stream->past_lines);  ++i) {
    if (Trace_stream->past_lines.at(i).depth == min_depth)
      Visible.insert(i);
  }
  tb_init();
  tb_clear();
  Display_row = Display_column = 0;
  Top_of_screen = 0;
  refresh_screen_rows();
  while (true) {
    render();
    int key = read_key();
    if (key == 'q' || key == 'Q' || key == TB_KEY_CTRL_C) break;
    if (key == 'j' || key == TB_KEY_ARROW_DOWN) {
      // move cursor one line down
      if (Display_row < Last_printed_row) ++Display_row;
    }
    else if (key == 'k' || key == TB_KEY_ARROW_UP) {
      // move cursor one line up
      if (Display_row > 0) --Display_row;
    }
    else if (key == 't') {
      // move cursor to top of screen
      Display_row = 0;
    }
    else if (key == 'c') {
      // move cursor to center of screen
      Display_row = tb_height()/2;
      while (!contains_key(Trace_index, Display_row))
        --Display_row;
    }
    else if (key == 'b') {
      // move cursor to bottom of screen
      Display_row = tb_height()-1;
      while (!contains_key(Trace_index, Display_row))
        --Display_row;
    }
    else if (key == 'T') {
      // scroll line at cursor to top of screen
      Top_of_screen = get(Trace_index, Display_row);
      Display_row = 0;
      refresh_screen_rows();
    }
    else if (key == 'h' || key == TB_KEY_ARROW_LEFT) {
      // pan screen one character left
      if (Left_of_screen > 0) --Left_of_screen;
    }
    else if (key == 'l' || key == TB_KEY_ARROW_RIGHT) {
      // pan screen one character right
      ++Left_of_screen;
    }
    else if (key == 'H') {
      // pan screen one screen-width left
      Left_of_screen -= (tb_width() - 5);
      if (Left_of_screen < 0) Left_of_screen = 0;
    }
    else if (key == 'L') {
      // pan screen one screen-width right
      Left_of_screen += (tb_width() - 5);
    }
    else if (key == 'J' || key == TB_KEY_PGDN || key == TB_KEY_CTRL_F) {
      // page-down
      if (Trace_index.find(tb_height()-1) != Trace_index.end()) {
        Top_of_screen = get(Trace_index, tb_height()-1) + 1;
        refresh_screen_rows();
      }
    }
    else if (key == 'K' || key == TB_KEY_PGUP || key == TB_KEY_CTRL_B) {
      // page-up is more convoluted
      for (int screen_row = tb_height();  screen_row > 0 && Top_of_screen > 0;  --screen_row) {
        --Top_of_screen;
        if (Top_of_screen <= 0) break;
        while (Top_of_screen > 0 && !contains_key(Visible, Top_of_screen))
          --Top_of_screen;
      }
      if (Top_of_screen >= 0)
        refresh_screen_rows();
    }
    else if (key == 'g' || key == TB_KEY_HOME) {
        Top_of_screen = 0;
        Last_printed_row = 0;
        Display_row = 0;
        refresh_screen_rows();
    }
    else if (key == 'G' || key == TB_KEY_END) {
      // go to bottom of screen; largely like page-up, interestingly
      Top_of_screen = SIZE(Trace_stream->past_lines)-1;
      for (int screen_row = tb_height();  screen_row > 0 && Top_of_screen > 0;  --screen_row) {
        --Top_of_screen;
        if (Top_of_screen <= 0) break;
        while (Top_of_screen > 0 && !contains_key(Visible, Top_of_screen))
          --Top_of_screen;
      }
      refresh_screen_rows();
      // move cursor to bottom
      Display_row = Last_printed_row;
      refresh_screen_rows();
    }
    else if (key == TB_KEY_CARRIAGE_RETURN) {
      // expand lines under current by one level
      assert(contains_key(Trace_index, Display_row));
      int start_index = get(Trace_index, Display_row);
      int index = 0;
      // simultaneously compute end_index and min_depth
      int min_depth = 9999;
      for (index = start_index+1;  index < SIZE(Trace_stream->past_lines);  ++index) {
        if (contains_key(Visible, index)) break;
        trace_line& curr_line = Trace_stream->past_lines.at(index);
        assert(curr_line.depth > Trace_stream->past_lines.at(start_index).depth);
        if (curr_line.depth < min_depth) min_depth = curr_line.depth;
      }
      int end_index = index;
      // mark as visible all intervening indices at min_depth
      for (index = start_index;  index < end_index;  ++index) {
        trace_line& curr_line = Trace_stream->past_lines.at(index);
        if (curr_line.depth == min_depth) {
          Visible.insert(index);
        }
      }
      refresh_screen_rows();
    }
    else if (key == TB_KEY_BACKSPACE || key == TB_KEY_BACKSPACE2) {
      // collapse all lines under current
      assert(contains_key(Trace_index, Display_row));
      int start_index = get(Trace_index, Display_row);
      int index = 0;
      // end_index is the next line at a depth same as or lower than start_index
      int initial_depth = Trace_stream->past_lines.at(start_index).depth;
      for (index = start_index+1;  index < SIZE(Trace_stream->past_lines);  ++index) {
        if (!contains_key(Visible, index)) continue;
        trace_line& curr_line = Trace_stream->past_lines.at(index);
        if (curr_line.depth <= initial_depth) break;
      }
      int end_index = index;
      // mark as visible all intervening indices at min_depth
      for (index = start_index+1;  index < end_index;  ++index) {
        Visible.erase(index);
      }
      refresh_screen_rows();
    }
    else if (key == '/') {
      if (start_search_editor(FORWARD))
        search(Current_search_pattern, Current_search_direction);
    }
    else if (key == '?') {
      if (start_search_editor(BACKWARD))
        search(Current_search_pattern, Current_search_direction);
    }
    else if (key == 'n') {
      if (!Current_search_pattern.empty())
        search(Current_search_pattern, Current_search_direction);
    }
    else if (key == 'N') {
      if (!Current_search_pattern.empty())
        search(Current_search_pattern, opposite(Current_search_direction));
    }
  }
  tb_shutdown();
}

bool start_search_editor(search_direction dir) {
  const int bottom_screen_line = tb_height()-1;
  // run a little editor just in the last line of the screen
  clear_line(bottom_screen_line);
  int col = 0;  // screen column of cursor on bottom line. also used to update pattern.
  tb_set_cursor(col, bottom_screen_line);
  tb_print('/', TB_WHITE, TB_BLACK);
  ++col;
  string pattern;
  while (true) {
    int key = read_key();
    if (key == TB_KEY_ENTER) {
      if (!pattern.empty()) {
        Current_search_pattern = pattern;
        Current_search_direction = dir;
      }
      return true;
    }
    else if (key == TB_KEY_ESC || key == TB_KEY_CTRL_C) {
      return false;
    }
    else if (key == TB_KEY_ARROW_LEFT) {
      if (col > /*slash*/1) {
        --col;
        tb_set_cursor(col, bottom_screen_line);
      }
    }
    else if (key == TB_KEY_ARROW_RIGHT) {
      if (col-/*slash*/1 < SIZE(pattern)) {
        ++col;
        tb_set_cursor(col, bottom_screen_line);
      }
    }
    else if (key == TB_KEY_HOME || key == TB_KEY_CTRL_A) {
      col = /*skip slash*/1;
      tb_set_cursor(col, bottom_screen_line);
    }
    else if (key == TB_KEY_END || key == TB_KEY_CTRL_E) {
      col = SIZE(pattern)+/*skip slash*/1;
      tb_set_cursor(col, bottom_screen_line);
    }
    else if (key == TB_KEY_BACKSPACE || key == TB_KEY_BACKSPACE2) {
      if (col > /*slash*/1) {
        assert(col <= SIZE(pattern)+1);
        --col;
        // update pattern
        pattern.erase(col-/*slash*/1, /*len*/1);
        // update screen
        tb_set_cursor(col, bottom_screen_line);
        for (int x = col;  x < SIZE(pattern)+/*skip slash*/1;  ++x)
          tb_print(pattern.at(x-/*slash*/1), TB_WHITE, TB_BLACK);
        tb_print(' ', TB_WHITE, TB_BLACK);
        tb_set_cursor(col, bottom_screen_line);
      }
    }
    else if (key == TB_KEY_CTRL_K) {
      int old_pattern_size = SIZE(pattern);
      pattern.erase(col-/*slash*/1, SIZE(pattern) - (col-/*slash*/1));
      tb_set_cursor(col, bottom_screen_line);
      for (int x = col;  x < old_pattern_size+/*slash*/1;  ++x)
        tb_print(' ', TB_WHITE, TB_BLACK);
      tb_set_cursor(col, bottom_screen_line);
    }
    else if (key == TB_KEY_CTRL_U) {
      int old_pattern_size = SIZE(pattern);
      pattern.erase(0, col-/*slash*/1);
      col = /*skip slash*/1;
      tb_set_cursor(col, bottom_screen_line);
      for (int x = /*slash*/1;  x < SIZE(pattern)+/*skip slash*/1;  ++x)
        tb_print(pattern.at(x-/*slash*/1), TB_WHITE, TB_BLACK);
      for (int x = SIZE(pattern)+/*slash*/1;  x < old_pattern_size+/*skip slash*/1;  ++x)
        tb_print(' ', TB_WHITE, TB_BLACK);
      tb_set_cursor(col, bottom_screen_line);
    }
    else if (key < 128) {  // ascii only
      // update pattern
      char c = static_cast<char>(key);
      assert(col-1 >= 0);
      assert(col-1 <= SIZE(pattern));
      pattern.insert(col-/*slash*/1, /*num*/1, c);
      // update screen
      for (int x = col;  x < SIZE(pattern)+/*skip slash*/1;  ++x)
        tb_print(pattern.at(x-/*slash*/1), TB_WHITE, TB_BLACK);
      ++col;
      tb_set_cursor(col, bottom_screen_line);
    }
  }
}

void search(const string& pat, search_direction dir) {
  if (dir == FORWARD) search_next(pat);
  else search_previous(pat);
}

search_direction opposite(search_direction dir) {
  if (dir == FORWARD) return BACKWARD;
  else return FORWARD;
}

void search_next(const string& pat) {
  for (int trace_index = get(Trace_index, Display_row)+1;  trace_index < SIZE(Trace_stream->past_lines);  ++trace_index) {
    if (!contains_key(Visible, trace_index)) continue;
    const trace_line& line = Trace_stream->past_lines.at(trace_index);
    if (line.label.find(pat) == string::npos && line.contents.find(pat) == string::npos) continue;
    Top_of_screen = trace_index;
    Display_row = 0;
    refresh_screen_rows();
    return;
  }
}

void search_previous(const string& pat) {
  for (int trace_index = get(Trace_index, Display_row)-1;  trace_index >= 0;  --trace_index) {
    if (!contains_key(Visible, trace_index)) continue;
    const trace_line& line = Trace_stream->past_lines.at(trace_index);
    if (line.label.find(pat) == string::npos && line.contents.find(pat) == string::npos) continue;
    Top_of_screen = trace_index;
    Display_row = 0;
    refresh_screen_rows();
    return;
  }
}

void clear_line(int screen_row) {
  tb_set_cursor(0, screen_row);
  for (int col = 0;  col < tb_width();  ++col)
    tb_print(' ', TB_WHITE, TB_BLACK);
  tb_set_cursor(0, screen_row);
}

// update Trace_indices for each screen_row on the basis of Top_of_screen and Visible
void refresh_screen_rows() {
  int screen_row = 0, index = 0;
  Trace_index.clear();
  for (screen_row = 0, index = Top_of_screen;  screen_row < tb_height() && index < SIZE(Trace_stream->past_lines);  ++screen_row, ++index) {
    // skip lines without depth for now
    while (!contains_key(Visible, index)) {
      ++index;
      if (index >= SIZE(Trace_stream->past_lines)) goto done;
    }
    assert(index < SIZE(Trace_stream->past_lines));
    put(Trace_index, screen_row, index);
  }
done:;
}

void render() {
  int screen_row = 0;
  for (screen_row = 0;  screen_row < tb_height();  ++screen_row) {
    if (!contains_key(Trace_index, screen_row)) break;
    trace_line& curr_line = Trace_stream->past_lines.at(get(Trace_index, screen_row));
    ostringstream out;
    out << std::setw(4) << curr_line.depth << ' ' << curr_line.label << ": " << curr_line.contents;
    if (screen_row < tb_height()-1) {
      int delta = lines_hidden(screen_row);
      // home-brew escape sequence for red
      if (delta > 1) {
        if (delta > 999) out << static_cast<char>(1);
        out << " (" << delta << ")";
        if (delta > 999) out << static_cast<char>(2);
      }
    }
    render_line(screen_row, out.str(), screen_row == Display_row);
  }
  // clear rest of screen
  Last_printed_row = screen_row-1;
  for (;  screen_row < tb_height();  ++screen_row)
    render_line(screen_row, "~", /*cursor_line?*/false);
  // move cursor back to display row at the end
  tb_set_cursor(0, Display_row);
}

int lines_hidden(int screen_row) {
  assert(contains_key(Trace_index, screen_row));
  if (!contains_key(Trace_index, screen_row+1))
    return SIZE(Trace_stream->past_lines) - get(Trace_index, screen_row);
  else
    return get(Trace_index, screen_row+1) - get(Trace_index, screen_row);
}

void render_line(int screen_row, const string& s, bool cursor_line) {
  int col = 0;
  int color = TB_WHITE;
  int background_color = cursor_line ? /*subtle grey*/240 : TB_BLACK;
  vector<pair<size_t, size_t> > highlight_ranges = find_all_occurrences(s, Current_search_pattern);
  tb_set_cursor(0, screen_row);
  for (col = 0;  col < tb_width() && col+Left_of_screen < SIZE(s);  ++col) {
    char c = s.at(col+Left_of_screen);  // todo: unicode
    if (c == '\n') c = ';';  // replace newlines with semi-colons
    // escapes. hack: can't start a line with them.
    if (c == '\1') { color = /*red*/1;  c = ' '; }
    if (c == '\2') { color = TB_WHITE;  c = ' '; }
    if (in_range(highlight_ranges, col+Left_of_screen))
      tb_print(c, TB_BLACK, /*yellow*/11);
    else
      tb_print(c, color, background_color);
  }
  for (;  col < tb_width();  ++col)
    tb_print(' ', TB_WHITE, background_color);
}

vector<pair<size_t, size_t> > find_all_occurrences(const string& s, const string& pat) {
  vector<pair<size_t, size_t> > result;
  if (pat.empty()) return result;
  size_t idx = 0;
  while (true) {
    size_t next_idx = s.find(pat, idx);
    if (next_idx == string::npos) break;
    result.push_back(pair<size_t, size_t>(next_idx, next_idx+SIZE(pat)));
    idx = next_idx+SIZE(pat);
  }
  return result;
}

bool in_range(const vector<pair<size_t, size_t> >& highlight_ranges, size_t idx) {
  for (int i = 0;  i < SIZE(highlight_ranges);  ++i) {
    if (idx >= highlight_ranges.at(i).first && idx < highlight_ranges.at(i).second)
      return true;
    if (idx < highlight_ranges.at(i).second) break;
  }
  return false;
}

void load_trace(const char* filename) {
  ifstream tin(filename);
  if (!tin) {
    cerr << "no such file: " << filename << '\n';
    exit(1);
  }
  Trace_stream = new trace_stream;
  while (has_data(tin)) {
    tin >> std::noskipws;
      skip_whitespace_but_not_newline(tin);
      if (!isdigit(tin.peek())) {
        string dummy;
        getline(tin, dummy);
        continue;
      }
    tin >> std::skipws;
    int depth;
    tin >> depth;
    string label;
    tin >> label;
    if (*--label.end() == ':') label.erase(--label.end());
    string line;
    getline(tin, line);
    Trace_stream->past_lines.push_back(trace_line(depth, label, line));
  }
  cerr << "lines read: " << Trace_stream->past_lines.size() << '\n';
}

int read_key() {
  tb_event event;
  do {
    tb_poll_event(&event);
  } while (event.type != TB_EVENT_KEY);
  return event.key ? event.key : event.ch;
}