summary refs log tree commit diff stats
path: root/ranger/core/loader.py
blob: 4f4424e4f79ca497051a8d3cdd21778d8b317ae8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# 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/>.

from collections import deque
from time import time
from ranger.shared import FileManagerAware
import math

def status_generator():
	"""Generate a rotating line which can be used as a throbber"""
	while True:
		yield '/'
		yield '-'
		yield '\\'
		yield '|'

class LoadableObject(object):
	def __init__(self, gen, descr):
		self.load_generator = gen
		self.description = descr

	def get_description(self):
		return self.description


class Loader(FileManagerAware):
	seconds_of_work_time = 0.03

	def __init__(self):
		self.queue = deque()
		self.item = None
		self.load_generator = None
		self.status_generator = status_generator()
		self.rotate()
		self.old_item = None

	def rotate(self):
		"""Rotate the throbber"""
		# TODO: move all throbber logic to UI
		self.status = next(self.status_generator)

	def add(self, obj):
		"""
		Add an object to the queue.
		It should have a load_generator method.
		"""
		while obj in self.queue:
			self.queue.remove(obj)
		self.queue.appendleft(obj)

	def move(self, _from, to):
		try:
			item = self.queue[_from]
		except IndexError:
			return

		del self.queue[_from]

		if to == 0:
			self.queue.appendleft(item)
		elif to == -1:
			self.queue.append(item)
		else:
			raise NotImplementedError

	def remove(self, item=None, index=None):
		if item is not None and index is None:
			for i, test in enumerate(self.queue):
				if test == item:
					index = i 
					break
			else:
				return

		if index is not None:
			if item is None:
				item = self.queue[index]
			if hasattr(item, 'unload'):
				item.unload()
			del self.queue[index]

	def work(self):
		"""
		Load items from the queue if there are any.
		Stop after approximately self.seconds_of_work_time.
		"""
		while True:
			# get the first item with a proper load_generator
			try:
				item = self.queue[0]
				if item.load_generator is None:
					self.queue.popleft()
				else:
					break
			except IndexError:
				return

		self.rotate()
		if item != self.old_item:
			self.old_item = item

		end_time = time() + self.seconds_of_work_time

		try:
			while time() < end_time:
				next(item.load_generator)
		except StopIteration:
			item.load_generator = None
			self.queue.remove(item)
		except Exception as err:
			self.fm.notify(err)

	def has_work(self):
		"""Is there anything to load?"""
		return bool(self.queue)
n>aercConf *config.AercConfig acctConf *config.AccountConfig store *lib.DirStore dirs []string logger *log.Logger selecting string selected string scroll int spinner *Spinner worker *types.Worker } func NewDirectoryList(conf *config.AercConfig, acctConf *config.AccountConfig, logger *log.Logger, worker *types.Worker) *DirectoryList { dirlist := &DirectoryList{ aercConf: conf, acctConf: acctConf, logger: logger, store: lib.NewDirStore(), worker: worker, } uiConf := dirlist.UiConfig() dirlist.spinner = NewSpinner(&uiConf) dirlist.spinner.OnInvalidate(func(_ ui.Drawable) { dirlist.Invalidate() }) dirlist.spinner.Start() return dirlist } func (dirlist *DirectoryList) UiConfig() config.UIConfig { return dirlist.aercConf.GetUiConfig(map[config.ContextType]string{ config.UI_CONTEXT_ACCOUNT: dirlist.acctConf.Name, config.UI_CONTEXT_FOLDER: dirlist.Selected(), }) } func (dirlist *DirectoryList) List() []string { return dirlist.store.List() } func (dirlist *DirectoryList) UpdateList(done func(dirs []string)) { // TODO: move this logic into dirstore var dirs []string dirlist.worker.PostAction( &types.ListDirectories{}, func(msg types.WorkerMessage) { switch msg := msg.(type) { case *types.Directory: dirs = append(dirs, msg.Dir.Name) case *types.Done: dirlist.store.Update(dirs) dirlist.filterDirsByFoldersConfig() dirlist.sortDirsByFoldersSortConfig() dirlist.store.Update(dirlist.dirs) dirlist.spinner.Stop() dirlist.Invalidate() if done != nil { done(dirs) } } }) } func (dirlist *DirectoryList) Select(name string) { dirlist.selecting = name dirlist.worker.PostAction(&types.OpenDirectory{Directory: name}, func(msg types.WorkerMessage) { switch msg.(type) { case *types.Error: dirlist.selecting = "" case *types.Done: dirlist.selected = dirlist.selecting dirlist.filterDirsByFoldersConfig() hasSelected := false for _, d := range dirlist.dirs { if d == dirlist.selected { hasSelected = true break } } if !hasSelected && dirlist.selected != "" { dirlist.dirs = append(dirlist.dirs, dirlist.selected) } sort.Strings(dirlist.dirs) dirlist.sortDirsByFoldersSortConfig() } dirlist.Invalidate() }) dirlist.Invalidate() } func (dirlist *DirectoryList) Selected() string { return dirlist.selected } func (dirlist *DirectoryList) Invalidate() { dirlist.DoInvalidate(dirlist) } func (dirlist *DirectoryList) getDirString(name string, width int, recentUnseen func() string) string { percent := false rightJustify := false formatted := "" doRightJustify := func(s string) { formatted = runewidth.FillRight(formatted, width-len(s)) formatted = runewidth.Truncate(formatted, width-len(s), "…") } for _, char := range dirlist.UiConfig().DirListFormat { switch char { case '%': if percent { formatted += string(char) percent = false } else { percent = true } case '>': if percent { rightJustify = true } case 'n': if percent { if rightJustify { doRightJustify(name) rightJustify = false } formatted += name percent = false } case 'r': if percent { rString := recentUnseen() if rightJustify { doRightJustify(rString) rightJustify = false } formatted += rString percent = false } default: formatted += string(char) } } return formatted } func (dirlist *DirectoryList) getRUEString(name string) string { msgStore, ok := dirlist.MsgStore(name) if !ok { return "" } var totalRecent, totalUnseen, totalExists int if msgStore.DirInfo.AccurateCounts { totalRecent = msgStore.DirInfo.Recent totalUnseen = msgStore.DirInfo.Unseen totalExists = msgStore.DirInfo.Exists } else { totalRecent, totalUnseen = countRUE(msgStore) // use the total count from the dirinfo, else we only count already // fetched messages totalExists = msgStore.DirInfo.Exists } rueString := "" if totalRecent > 0 { rueString = fmt.Sprintf("%d/%d/%d", totalRecent, totalUnseen, totalExists) } else if totalUnseen > 0 { rueString = fmt.Sprintf("%d/%d", totalUnseen, totalExists) } else if totalExists > 0 { rueString = fmt.Sprintf("%d", totalExists) } return rueString } func (dirlist *DirectoryList) Draw(ctx *ui.Context) { ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', dirlist.UiConfig().GetStyle(config.STYLE_DIRLIST_DEFAULT)) if dirlist.spinner.IsRunning() { dirlist.spinner.Draw(ctx) return } if len(dirlist.dirs) == 0 { style := dirlist.UiConfig().GetStyle(config.STYLE_DIRLIST_DEFAULT) ctx.Printf(0, 0, style, dirlist.UiConfig().EmptyDirlist) return } dirlist.ensureScroll(ctx.Height()) needScrollbar := true percentVisible := float64(ctx.Height()) / float64(len(dirlist.dirs)) if percentVisible >= 1.0 { needScrollbar = false } textWidth := ctx.Width() if needScrollbar { textWidth -= 1 } if textWidth < 0 { textWidth = 0 } for i, name := range dirlist.dirs { if i < dirlist.scroll { continue } row := i - dirlist.scroll if row >= ctx.Height() { break } style := tcell.StyleDefault if name == dirlist.selected { style = dirlist.UiConfig().GetStyleSelected(config.STYLE_DIRLIST_DEFAULT) } ctx.Fill(0, row, textWidth, 1, ' ', style) dirString := dirlist.getDirString(name, textWidth, func() string { return dirlist.getRUEString(name) }) ctx.Printf(0, row, style, dirString) } if needScrollbar { scrollBarCtx := ctx.Subcontext(ctx.Width()-1, 0, 1, ctx.Height()) dirlist.drawScrollbar(scrollBarCtx, percentVisible) } } func (dirlist *DirectoryList) drawScrollbar(ctx *ui.Context, percentVisible float64) { gutterStyle := tcell.StyleDefault pillStyle := tcell.StyleDefault.Reverse(true) // gutter ctx.Fill(0, 0, 1, ctx.Height(), ' ', gutterStyle) // pill pillSize := int(math.Ceil(float64(ctx.Height()) * percentVisible)) percentScrolled := float64(dirlist.scroll) / float64(len(dirlist.dirs)) pillOffset := int(math.Floor(float64(ctx.Height()) * percentScrolled)) ctx.Fill(0, pillOffset, 1, pillSize, ' ', pillStyle) } func (dirlist *DirectoryList) ensureScroll(h int) { selectingIdx := findString(dirlist.dirs, dirlist.selecting) if selectingIdx < 0 { // dir not found, meaning we are currently adding / removing a dir. // we can simply ignore this until we get redrawn with the new // dirlist.dir content return } maxScroll := len(dirlist.dirs) - h if maxScroll < 0 { maxScroll = 0 } if selectingIdx >= dirlist.scroll && selectingIdx < dirlist.scroll+h { if dirlist.scroll > maxScroll { dirlist.scroll = maxScroll } return } if selectingIdx >= dirlist.scroll+h { dirlist.scroll = selectingIdx - h + 1 } else if selectingIdx < dirlist.scroll { dirlist.scroll = selectingIdx } if dirlist.scroll > maxScroll { dirlist.scroll = maxScroll } } func (dirlist *DirectoryList) MouseEvent(localX int, localY int, event tcell.Event) { switch event := event.(type) { case *tcell.EventMouse: switch event.Buttons() { case tcell.Button1: clickedDir, ok := dirlist.Clicked(localX, localY) if ok { dirlist.Select(clickedDir) } case tcell.WheelDown: dirlist.Next() case tcell.WheelUp: dirlist.Prev() } } } func (dirlist *DirectoryList) Clicked(x int, y int) (string, bool) { if dirlist.dirs == nil || len(dirlist.dirs) == 0 { return "", false } for i, name := range dirlist.dirs { if i == y { return name, true } } return "", false } func (dirlist *DirectoryList) NextPrev(delta int) { curIdx := findString(dirlist.dirs, dirlist.selected) if curIdx == len(dirlist.dirs) { return } newIdx := curIdx + delta ndirs := len(dirlist.dirs) if ndirs == 0 { return } if newIdx < 0 { newIdx = ndirs - 1 } else if newIdx >= ndirs { newIdx = 0 } dirlist.Select(dirlist.dirs[newIdx]) } func (dirlist *DirectoryList) Next() { dirlist.NextPrev(1) } func (dirlist *DirectoryList) Prev() { dirlist.NextPrev(-1) } func folderMatches(folder string, pattern string) bool { if len(pattern) == 0 { return false } if pattern[0] == '~' { r, err := regexp.Compile(pattern[1:]) if err != nil { return false } return r.Match([]byte(folder)) } return pattern == folder } // sortDirsByFoldersSortConfig sets dirlist.dirs to be sorted based on the // AccountConfig.FoldersSort option. Folders not included in the option // will be appended at the end in alphabetical order func (dirlist *DirectoryList) sortDirsByFoldersSortConfig() { sort.Slice(dirlist.dirs, func(i, j int) bool { foldersSort := dirlist.acctConf.FoldersSort iInFoldersSort := findString(foldersSort, dirlist.dirs[i]) jInFoldersSort := findString(foldersSort, dirlist.dirs[j]) if iInFoldersSort >= 0 && jInFoldersSort >= 0 { return iInFoldersSort < jInFoldersSort } if iInFoldersSort >= 0 { return true } if jInFoldersSort >= 0 { return false } return dirlist.dirs[i] < dirlist.dirs[j] }) } // filterDirsByFoldersConfig sets dirlist.dirs to the filtered subset of the // dirstore, based on AccountConfig.Folders (inclusion) and // AccountConfig.FoldersExclude (exclusion), in that order. func (dirlist *DirectoryList) filterDirsByFoldersConfig() { filterDirs := func(orig, filters []string, exclude bool) []string { if len(filters) == 0 { return orig } var dest []string for _, folder := range orig { // When excluding, include things by default, and vice-versa include := exclude for _, f := range filters { if folderMatches(folder, f) { // If matched an exclusion, don't include // If matched an inclusion, do include include = !exclude break } } if include { dest = append(dest, folder) } } return dest } dirlist.dirs = dirlist.store.List() // 'folders' (if available) is used to make the initial list and // 'folders-exclude' removes from that list. configFolders := dirlist.acctConf.Folders dirlist.dirs = filterDirs(dirlist.dirs, configFolders, false) configFoldersExclude := dirlist.acctConf.FoldersExclude dirlist.dirs = filterDirs(dirlist.dirs, configFoldersExclude, true) } func (dirlist *DirectoryList) SelectedMsgStore() (*lib.MessageStore, bool) { return dirlist.store.MessageStore(dirlist.selected) } func (dirlist *DirectoryList) MsgStore(name string) (*lib.MessageStore, bool) { return dirlist.store.MessageStore(name) } func (dirlist *DirectoryList) SetMsgStore(name string, msgStore *lib.MessageStore) { dirlist.store.SetMessageStore(name, msgStore) msgStore.OnUpdateDirs(func() { dirlist.Invalidate() }) } func findString(slice []string, str string) int { for i, s := range slice { if str == s { return i } } return -1 } func (dirlist *DirectoryList) getSortCriteria() []*types.SortCriterion { if len(dirlist.UiConfig().Sort) == 0 { return nil } criteria, err := libsort.GetSortCriteria(dirlist.UiConfig().Sort) if err != nil { dirlist.logger.Printf("getSortCriteria failed: %v", err) return nil } return criteria } func countRUE(msgStore *lib.MessageStore) (recent, unread int) { for _, msg := range msgStore.Messages { if msg == nil { continue } seen := false isrecent := false for _, flag := range msg.Flags { if flag == models.SeenFlag { seen = true } else if flag == models.RecentFlag { isrecent = true } } if !seen { if isrecent { recent++ } else { unread++ } } } return recent, unread }