git-cola v2.11
[git-cola.git] / cola / app.py
blob16f6df1ca21b6a2b497473091bca5bcd63ebd11f
1 """Provides the main() routine and ColaApplication"""
2 from __future__ import division, absolute_import, unicode_literals
3 import argparse
4 import os
5 import signal
6 import sys
8 __copyright__ = """
9 Copyright (C) 2009-2016 David Aguilar and contributors
10 """
12 from . import core
13 try:
14 from qtpy import QtCore
15 except ImportError:
16 errmsg = """
17 Sorry, you do not seem to have PyQt5, Pyside, or PyQt4 installed.
18 Please install it before using git-cola, e.g.:
19 $ sudo apt-get install python-qt4
20 """
21 core.error(errmsg)
23 from qtpy import QtGui
24 from qtpy import QtWidgets
26 # Import cola modules
27 from .decorators import memoize
28 from .i18n import N_
29 from .interaction import Interaction
30 from .models import main
31 from .widgets import cfgactions
32 from .widgets import defs
33 from .widgets import startup
34 from .settings import Session
35 from . import cmds
36 from . import core
37 from . import compat
38 from . import fsmonitor
39 from . import git
40 from . import gitcfg
41 from . import icons
42 from . import i18n
43 from . import qtcompat
44 from . import qtutils
45 from . import resources
46 from . import version
49 def setup_environment():
50 # Allow Ctrl-C to exit
51 signal.signal(signal.SIGINT, signal.SIG_DFL)
53 # Session management wants an absolute path when restarting
54 sys.argv[0] = sys_argv0 = os.path.abspath(sys.argv[0])
56 # Spoof an X11 display for SSH
57 os.environ.setdefault('DISPLAY', ':0')
59 if not core.getenv('SHELL', ''):
60 for shell in ('/bin/zsh', '/bin/bash', '/bin/sh'):
61 if os.path.exists(shell):
62 compat.setenv('SHELL', shell)
63 break
65 # Setup the path so that git finds us when we run 'git cola'
66 path_entries = core.getenv('PATH', '').split(os.pathsep)
67 bindir = core.decode(os.path.dirname(sys_argv0))
68 path_entries.append(bindir)
69 path = os.pathsep.join(path_entries)
70 compat.setenv('PATH', path)
72 # We don't ever want a pager
73 compat.setenv('GIT_PAGER', '')
75 # Setup *SSH_ASKPASS
76 git_askpass = core.getenv('GIT_ASKPASS')
77 ssh_askpass = core.getenv('SSH_ASKPASS')
78 if git_askpass:
79 askpass = git_askpass
80 elif ssh_askpass:
81 askpass = ssh_askpass
82 elif sys.platform == 'darwin':
83 askpass = resources.share('bin', 'ssh-askpass-darwin')
84 else:
85 askpass = resources.share('bin', 'ssh-askpass')
87 compat.setenv('GIT_ASKPASS', askpass)
88 compat.setenv('SSH_ASKPASS', askpass)
90 # --- >8 --- >8 ---
91 # Git v1.7.10 Release Notes
92 # =========================
94 # Compatibility Notes
95 # -------------------
97 # * From this release on, the "git merge" command in an interactive
98 # session will start an editor when it automatically resolves the
99 # merge for the user to explain the resulting commit, just like the
100 # "git commit" command does when it wasn't given a commit message.
102 # If you have a script that runs "git merge" and keeps its standard
103 # input and output attached to the user's terminal, and if you do not
104 # want the user to explain the resulting merge commits, you can
105 # export GIT_MERGE_AUTOEDIT environment variable set to "no", like
106 # this:
108 # #!/bin/sh
109 # GIT_MERGE_AUTOEDIT=no
110 # export GIT_MERGE_AUTOEDIT
112 # to disable this behavior (if you want your users to explain their
113 # merge commits, you do not have to do anything). Alternatively, you
114 # can give the "--no-edit" option to individual invocations of the
115 # "git merge" command if you know everybody who uses your script has
116 # Git v1.7.8 or newer.
117 # --- >8 --- >8 ---
118 # Longer-term: Use `git merge --no-commit` so that we always
119 # have a chance to explain our merges.
120 compat.setenv('GIT_MERGE_AUTOEDIT', 'no')
123 def get_icon_themes():
124 """Return the default icon theme names"""
125 themes = []
127 icon_themes_env = core.getenv('GIT_COLA_ICON_THEME')
128 if icon_themes_env:
129 themes.extend([x for x in icon_themes_env.split(':') if x])
131 icon_themes_cfg = gitcfg.current().get_all('cola.icontheme')
132 if icon_themes_cfg:
133 themes.extend(icon_themes_cfg)
135 if not themes:
136 themes.append('light')
138 return themes
141 # style note: we use camelCase here since we're masquerading a Qt class
142 class ColaApplication(object):
143 """The main cola application
145 ColaApplication handles i18n of user-visible data
148 def __init__(self, argv, locale=None, gui=True, icon_themes=None):
149 cfgactions.install()
150 i18n.install(locale)
151 qtcompat.install()
152 qtutils.install()
153 icons.install(icon_themes or get_icon_themes())
155 fsmonitor.current().files_changed.connect(self._update_files)
157 if gui:
158 self._app = current(tuple(argv))
159 self._app.setWindowIcon(icons.cola())
160 self._install_style()
161 else:
162 self._app = QtCore.QCoreApplication(argv)
164 def _install_style(self):
165 palette = self._app.palette()
166 window = palette.color(QtGui.QPalette.Window)
167 highlight = palette.color(QtGui.QPalette.Highlight)
168 shadow = palette.color(QtGui.QPalette.Shadow)
169 base = palette.color(QtGui.QPalette.Base)
171 window_rgb = qtutils.rgb_css(window)
172 highlight_rgb = qtutils.rgb_css(highlight)
173 shadow_rgb = qtutils.rgb_css(shadow)
174 base_rgb = qtutils.rgb_css(base)
176 self._app.setStyleSheet("""
177 QCheckBox::indicator {
178 width: %(checkbox_size)spx;
179 height: %(checkbox_size)spx;
181 QCheckBox::indicator::unchecked {
182 border: %(checkbox_border)spx solid %(shadow_rgb)s;
183 background: %(base_rgb)s;
185 QCheckBox::indicator::checked {
186 image: url(%(checkbox_icon)s);
187 border: %(checkbox_border)spx solid %(shadow_rgb)s;
188 background: %(base_rgb)s;
191 QRadioButton::indicator {
192 width: %(radio_size)spx;
193 height: %(radio_size)spx;
195 QRadioButton::indicator::unchecked {
196 border: %(radio_border)spx solid %(shadow_rgb)s;
197 border-radius: %(radio_radius)spx;
198 background: %(base_rgb)s;
200 QRadioButton::indicator::checked {
201 image: url(%(radio_icon)s);
202 border: %(radio_border)spx solid %(shadow_rgb)s;
203 border-radius: %(radio_radius)spx;
204 background: %(base_rgb)s;
207 QSplitter::handle:hover {
208 background: %(highlight_rgb)s;
211 QMainWindow::separator {
212 background: %(window_rgb)s;
213 width: %(separator)spx;
214 height: %(separator)spx;
216 QMainWindow::separator:hover {
217 background: %(highlight_rgb)s;
220 """ % dict(separator=defs.separator,
221 window_rgb=window_rgb,
222 highlight_rgb=highlight_rgb,
223 shadow_rgb=shadow_rgb,
224 base_rgb=base_rgb,
225 checkbox_border=defs.border,
226 checkbox_icon=icons.check_name(),
227 checkbox_size=defs.checkbox,
228 radio_border=defs.radio_border,
229 radio_icon=icons.dot_name(),
230 radio_radius=defs.checkbox//2,
231 radio_size=defs.checkbox))
233 def activeWindow(self):
234 """Wrap activeWindow()"""
235 return self._app.activeWindow()
237 def desktop(self):
238 return self._app.desktop()
240 def exec_(self):
241 """Wrap exec_()"""
242 return self._app.exec_()
244 def set_view(self, view):
245 if hasattr(self._app, 'view'):
246 self._app.view = view
248 def _update_files(self):
249 # Respond to file system updates
250 cmds.do(cmds.Refresh)
253 @memoize
254 def current(argv):
255 return ColaQApplication(list(argv))
258 class ColaQApplication(QtWidgets.QApplication):
260 def __init__(self, argv):
261 super(ColaQApplication, self).__init__(argv)
262 self.view = None # injected by application_start()
264 def event(self, e):
265 if e.type() == QtCore.QEvent.ApplicationActivate:
266 cfg = gitcfg.current()
267 if cfg.get('cola.refreshonfocus', False):
268 cmds.do(cmds.Refresh)
269 return super(ColaQApplication, self).event(e)
271 def commitData(self, session_mgr):
272 """Save session data"""
273 if self.view is None:
274 return
275 sid = session_mgr.sessionId()
276 skey = session_mgr.sessionKey()
277 session_id = '%s_%s' % (sid, skey)
278 session = Session(session_id, repo=core.getcwd())
279 self.view.save_state(settings=session)
282 def process_args(args):
283 if args.version:
284 # Accept 'git cola --version' or 'git cola version'
285 version.print_version()
286 sys.exit(core.EXIT_SUCCESS)
288 # Handle session management
289 restore_session(args)
291 # Bail out if --repo is not a directory
292 repo = core.decode(args.repo)
293 if repo.startswith('file:'):
294 repo = repo[len('file:'):]
295 repo = core.realpath(repo)
296 if not core.isdir(repo):
297 errmsg = N_('fatal: "%s" is not a directory. '
298 'Please specify a correct --repo <path>.') % repo
299 core.stderr(errmsg)
300 sys.exit(core.EXIT_USAGE)
303 def restore_session(args):
304 # args.settings is provided when restoring from a session.
305 args.settings = None
306 if args.session is None:
307 return
308 session = Session(args.session)
309 if session.load():
310 args.settings = session
311 args.repo = session.repo
314 def application_init(args, update=False):
315 """Parses the command-line arguments and starts git-cola
317 # Ensure that we're working in a valid git repository.
318 # If not, try to find one. When found, chdir there.
319 setup_environment()
320 process_args(args)
322 app = new_application(args)
323 model = new_model(app, args.repo,
324 prompt=args.prompt, settings=args.settings)
325 if update:
326 model.update_status()
327 cfg = gitcfg.current()
328 return ApplicationContext(args, app, cfg, model)
331 def application_start(context, view):
332 """Show the GUI and start the main event loop"""
333 # Store the view for session management
334 context.app.set_view(view)
336 # Make sure that we start out on top
337 view.show()
338 view.raise_()
340 # Scan for the first time
341 runtask = qtutils.RunTask(parent=view)
342 init_update_task(view, runtask, context.model)
344 # Start the filesystem monitor thread
345 fsmonitor.current().start()
347 QtCore.QTimer.singleShot(0, _send_msg)
349 # Start the event loop
350 result = context.app.exec_()
352 # All done, cleanup
353 fsmonitor.current().stop()
354 QtCore.QThreadPool.globalInstance().waitForDone()
356 return result
359 def add_common_arguments(parser):
360 # We also accept 'git cola version'
361 parser.add_argument('--version', default=False, action='store_true',
362 help='print version number')
364 # Specifies a git repository to open
365 parser.add_argument('-r', '--repo', metavar='<repo>', default=core.getcwd(),
366 help='open the specified git repository')
368 # Specifies that we should prompt for a repository at startup
369 parser.add_argument('--prompt', action='store_true', default=False,
370 help='prompt for a repository')
372 # Specify the icon theme
373 parser.add_argument('--icon-theme', metavar='<theme>',
374 dest='icon_themes', action='append', default=[],
375 help='specify an icon theme (name or directory)')
377 # Resume an X Session Management session
378 parser.add_argument('-session', metavar='<session>', default=None,
379 help=argparse.SUPPRESS)
382 def new_application(args):
383 # Initialize the app
384 return ColaApplication(sys.argv, icon_themes=args.icon_themes)
387 def new_model(app, repo, prompt=False, settings=None):
388 model = main.model()
389 valid = False
390 if not prompt:
391 valid = model.set_worktree(repo)
392 if not valid:
393 # We are not currently in a git repository so we need to find one.
394 # Before prompting the user for a repository, check if they've
395 # configured a default repository and attempt to use it.
396 default_repo = gitcfg.current().get('cola.defaultrepo')
397 if default_repo:
398 valid = model.set_worktree(default_repo)
400 while not valid:
401 # If we've gotten into this loop then that means that neither the
402 # current directory nor the default repository were available.
403 # Prompt the user for a repository.
404 startup_dlg = startup.StartupDialog(app.activeWindow(),
405 settings=settings)
406 gitdir = startup_dlg.find_git_repo()
407 if not gitdir:
408 sys.exit(core.EXIT_NOINPUT)
409 valid = model.set_worktree(gitdir)
411 return model
414 def init_update_task(parent, runtask, model):
415 """Update the model in the background
417 git-cola should startup as quickly as possible.
421 def update_status():
422 model.update_status(update_index=True)
424 task = qtutils.SimpleTask(parent, update_status)
425 runtask.start(task)
428 def _send_msg():
429 trace = git.GIT_COLA_TRACE
430 if trace == '2' or trace == 'trace':
431 msg1 = 'info: debug level 2: trace mode enabled'
432 msg2 = 'info: set GIT_COLA_TRACE=1 for less-verbose output'
433 Interaction.log(msg1)
434 Interaction.log(msg2)
435 elif trace:
436 msg1 = 'info: debug level 1'
437 msg2 = 'info: set GIT_COLA_TRACE=2 for trace mode'
438 Interaction.log(msg1)
439 Interaction.log(msg2)
442 class ApplicationContext(object):
444 def __init__(self, args, app, cfg, model):
445 self.args = args
446 self.app = app
447 self.cfg = cfg
448 self.model = model