Merge pull request #31 from MightyCreak/split-code-into-modules
Start to modularize code
This commit is contained in:
commit
ea231a3444
|
@ -26,12 +26,15 @@ jobs:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-python@v2
|
- uses: actions/setup-python@v2
|
||||||
|
|
||||||
- name: Meson Build
|
- name: Install dependencies
|
||||||
uses: BSFishy/meson-build@v1.0.1
|
run: sudo apt-get -y install appstream appstream-util desktop-file-utils gettext
|
||||||
|
|
||||||
|
- name: Meson build
|
||||||
|
uses: BSFishy/meson-build@v1.0.3
|
||||||
with:
|
with:
|
||||||
action: build
|
action: build
|
||||||
|
|
||||||
- name: Meson Test
|
- name: Meson test
|
||||||
uses: BSFishy/meson-build@v1.0.1
|
uses: BSFishy/meson-build@v1.0.3
|
||||||
with:
|
with:
|
||||||
action: test
|
action: test
|
||||||
|
|
|
@ -13,8 +13,7 @@ __pycache__/
|
||||||
# Distribution / packaging
|
# Distribution / packaging
|
||||||
.Python
|
.Python
|
||||||
build/
|
build/
|
||||||
builddir/
|
build-flatpak/
|
||||||
builddir-flatpak/
|
|
||||||
develop-eggs/
|
develop-eggs/
|
||||||
dist/
|
dist/
|
||||||
downloads/
|
downloads/
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Python: Debug",
|
||||||
|
"type": "python",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/main.py",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
// "env": {
|
||||||
|
// "PYTHONPATH": "."
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Python: Remote Attach",
|
||||||
|
"type": "python",
|
||||||
|
"request": "attach",
|
||||||
|
"port": 5678,
|
||||||
|
"host": "localhost",
|
||||||
|
"pathMappings": [
|
||||||
|
{
|
||||||
|
"localRoot": "${workspaceFolder}",
|
||||||
|
"remoteRoot": "."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
### Added
|
### Added
|
||||||
- Added MetaInfo file
|
- Added MetaInfo file
|
||||||
- New SVG icon (thanks @creepertron95, @jimmac and @freddii)
|
- New SVG icon (thanks @creepertron95, @jimmac and @freddii)
|
||||||
|
- Started modularizing the code
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Changed AppID to io.github.mightycreak.Diffuse (as explained in
|
- Changed AppID to io.github.mightycreak.Diffuse (as explained in
|
||||||
|
@ -21,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Add .desktop translations in .po files
|
- Add .desktop translations in .po files
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
- Fixed some GTK deprecation warnings
|
||||||
|
|
||||||
## [0.6.0] - 2020-11-29
|
## [0.6.0] - 2020-11-29
|
||||||
|
|
||||||
|
|
79
README.md
79
README.md
|
@ -20,48 +20,71 @@ Some key features of Diffuse:
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
Diffuse is implemented entirely in Python and should run on any platform with
|
||||||
|
Python and PyGObject.
|
||||||
|
|
||||||
* Python >= 3.4
|
* Python >= 3.4
|
||||||
* PyGObject >= 3.18
|
* PyGObject >= 3.18
|
||||||
|
|
||||||
Diffuse is implemented entirely in Python and should run on any platform with
|
## Users
|
||||||
Python and PyGTK. If you need to manually install PyGTK, please be aware its
|
|
||||||
dependencies should be installed prior to installing PyGTK.
|
|
||||||
|
|
||||||
Diffuse can be run directly from an untared source distribution on any POSIX
|
### Installing using Flatpak
|
||||||
system or installed with the instructions described in the next section.
|
|
||||||
|
|
||||||
The location of the personal preferences, state, and initialisation files have
|
This is the easiest way to install Diffuse:
|
||||||
changed in the 0.4.1 release. Old settings may be migrated using the following
|
|
||||||
commands:
|
|
||||||
|
|
||||||
$ mkdir -p ~/.config/diffuse
|
```sh
|
||||||
$ mv ~/.diffuse/config ~/.config/diffuse/state
|
flatpak install io.github.mightycreak.Diffuse
|
||||||
$ mv ~/.diffuse/* ~/.config/diffuse
|
```
|
||||||
$ rmdir ~/.diffuse
|
|
||||||
|
|
||||||
The rules for parsing files in `~/.diffuse` changed in the 0.3.0 release.
|
## Developers
|
||||||
Non-fatal errors may be reported when parsing old files. These errors can be
|
|
||||||
fixed by removing the offending lines (or the entire file) from
|
|
||||||
`~/.config/diffuse/diffuserc`.
|
|
||||||
|
|
||||||
## Installing on POSIX systems
|
### Setup
|
||||||
|
|
||||||
|
#### Run Diffuse from source
|
||||||
|
|
||||||
|
To run Diffuse from the source code, type this:
|
||||||
|
```sh
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
To debug with VS Code, open the directory in VS Code, place your breakpoints and hit F5.
|
||||||
|
|
||||||
|
#### Build Diffuse
|
||||||
|
|
||||||
|
To build Diffuse, type this:
|
||||||
|
```sh
|
||||||
|
python setup.py build
|
||||||
|
```
|
||||||
|
|
||||||
|
To run from the build, type this:
|
||||||
|
```sh
|
||||||
|
PYTHONPATH=build/lib ./build/scripts-3.7/diffuse
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Install Diffuse locally
|
||||||
|
|
||||||
Diffuse build system is meson.
|
Diffuse build system is meson.
|
||||||
|
|
||||||
To install diffuse locally:
|
To install diffuse locally:
|
||||||
|
|
||||||
meson builddir
|
```sh
|
||||||
meson install -C builddir
|
meson setup build
|
||||||
|
cd build
|
||||||
|
meson compile
|
||||||
|
meson install # requires admin privileges
|
||||||
|
```
|
||||||
|
|
||||||
To uninstall diffuse afterwards:
|
To uninstall diffuse afterwards:
|
||||||
|
|
||||||
sudo ninja uninstall -C builddir
|
```sh
|
||||||
|
sudo ninja uninstall -C build
|
||||||
sudo rm -v /usr/local/share/locale/*/LC_MESSAGES/diffuse.mo
|
sudo rm -v /usr/local/share/locale/*/LC_MESSAGES/diffuse.mo
|
||||||
|
```
|
||||||
|
|
||||||
Meson allows to change the default installation directories, see
|
Meson allows to change the default installation directories, see
|
||||||
[command-line documentation](https://mesonbuild.com/Commands.html#configure).
|
[command-line documentation](https://mesonbuild.com/Commands.html#configure).
|
||||||
|
|
||||||
## Installing on Windows
|
### Installing on Windows
|
||||||
|
|
||||||
The `windows-installer` directory contains scripts for building an installable
|
The `windows-installer` directory contains scripts for building an installable
|
||||||
package for Windows that includes all dependencies.
|
package for Windows that includes all dependencies.
|
||||||
|
@ -73,24 +96,26 @@ Diffuse. The `XDG_CONFIG_HOME` and `XDG_DATA_DIR` environment variables
|
||||||
indicate where Diffuse should store persistent settings (eg. the path to a
|
indicate where Diffuse should store persistent settings (eg. the path to a
|
||||||
writable directory on the pen drive).
|
writable directory on the pen drive).
|
||||||
|
|
||||||
## Installing the Flatpak package
|
|
||||||
|
|
||||||
flatpak install io.github.mightycreak.Diffuse
|
|
||||||
|
|
||||||
## Building and testing the Flatpak package
|
## Building and testing the Flatpak package
|
||||||
|
|
||||||
To install Diffuse locally:
|
To install Diffuse locally:
|
||||||
|
|
||||||
flatpak install flatpak install runtime/org.gnome.Sdk/$(uname -p)/3.38
|
```sh
|
||||||
flatpak-builder builddir-flatpak --user --install io.github.mightycreak.Diffuse.yml
|
flatpak install runtime/org.gnome.Sdk/$(uname -p)/3.38
|
||||||
|
flatpak-builder build-flatpak --user --install io.github.mightycreak.Diffuse.yml
|
||||||
|
```
|
||||||
|
|
||||||
To run Diffuse through Flatpak:
|
To run Diffuse through Flatpak:
|
||||||
|
|
||||||
|
```sh
|
||||||
flatpak run io.github.mightycreak.Diffuse
|
flatpak run io.github.mightycreak.Diffuse
|
||||||
|
```
|
||||||
|
|
||||||
To uninstall Diffuse:
|
To uninstall Diffuse:
|
||||||
|
|
||||||
|
```sh
|
||||||
flatpak remove io.github.mightycreak.Diffuse
|
flatpak remove io.github.mightycreak.Diffuse
|
||||||
|
```
|
||||||
|
|
||||||
## Help Documentation
|
## Help Documentation
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import sysconfig
|
|
||||||
from compileall import compile_dir
|
|
||||||
from os import environ, path
|
from os import environ, path
|
||||||
from subprocess import call
|
from subprocess import call
|
||||||
|
|
||||||
|
@ -13,3 +11,9 @@ destdir = environ.get('DESTDIR', '')
|
||||||
if not destdir:
|
if not destdir:
|
||||||
print('Updating icon cache...')
|
print('Updating icon cache...')
|
||||||
call(['gtk-update-icon-cache', '-qtf', path.join(datadir, 'icons', 'hicolor')])
|
call(['gtk-update-icon-cache', '-qtf', path.join(datadir, 'icons', 'hicolor')])
|
||||||
|
|
||||||
|
print('Updating desktop database...')
|
||||||
|
call(['update-desktop-database', '-q', path.join(datadir, 'applications')])
|
||||||
|
|
||||||
|
print('Compiling GSettings schemas...')
|
||||||
|
call(['glib-compile-schemas', path.join(datadir, 'glib-2.0', 'schemas')])
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
@ -0,0 +1,13 @@
|
||||||
|
application_id = 'io.github.mightycreak.Diffuse'
|
||||||
|
|
||||||
|
scalable_dir = join_paths('hicolor', 'scalable', 'apps')
|
||||||
|
install_data(
|
||||||
|
join_paths(scalable_dir, ('@0@.svg').format(application_id)),
|
||||||
|
install_dir: join_paths(get_option('datadir'), 'icons', scalable_dir)
|
||||||
|
)
|
||||||
|
|
||||||
|
symbolic_dir = join_paths('hicolor', 'symbolic', 'apps')
|
||||||
|
install_data(
|
||||||
|
join_paths(symbolic_dir, ('@0@-symbolic.svg').format(application_id)),
|
||||||
|
install_dir: join_paths(get_option('datadir'), 'icons', symbolic_dir)
|
||||||
|
)
|
|
@ -1,7 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<component type="desktop-application">
|
<component type="desktop-application">
|
||||||
<id>io.github.mightycreak.Diffuse</id>
|
<id>io.github.mightycreak.Diffuse</id>
|
||||||
|
|
||||||
<name>Diffuse Merge Tool</name>
|
<name>Diffuse Merge Tool</name>
|
||||||
<summary>Graphical tool for merging and comparing text files</summary>
|
<summary>Graphical tool for merging and comparing text files</summary>
|
||||||
<description>
|
<description>
|
||||||
|
@ -9,23 +8,18 @@
|
||||||
Diffuse is a graphical tool for comparing and merging text files. It can retrieve files for comparison from Bazaar, CVS, Darcs, Git, Mercurial, Monotone, RCS, Subversion, and SVK repositories.
|
Diffuse is a graphical tool for comparing and merging text files. It can retrieve files for comparison from Bazaar, CVS, Darcs, Git, Mercurial, Monotone, RCS, Subversion, and SVK repositories.
|
||||||
</p>
|
</p>
|
||||||
</description>
|
</description>
|
||||||
|
|
||||||
<metadata_license>FSFAP</metadata_license>
|
<metadata_license>FSFAP</metadata_license>
|
||||||
<project_license>GPL-2.0-or-later</project_license>
|
<project_license>GPL-2.0-or-later</project_license>
|
||||||
<content_rating type="oars-1.1"/>
|
<content_rating type="oars-1.1"/>
|
||||||
|
|
||||||
<launchable type="desktop-id">io.github.mightycreak.Diffuse.desktop</launchable>
|
<launchable type="desktop-id">io.github.mightycreak.Diffuse.desktop</launchable>
|
||||||
|
|
||||||
<url type="homepage">https://mightycreak.github.io/diffuse/</url>
|
<url type="homepage">https://mightycreak.github.io/diffuse/</url>
|
||||||
<url type="bugtracker">https://github.com/MightyCreak/diffuse/issues</url>
|
<url type="bugtracker">https://github.com/MightyCreak/diffuse/issues</url>
|
||||||
|
|
||||||
<screenshots>
|
<screenshots>
|
||||||
<screenshot type="default">
|
<screenshot type="default">
|
||||||
<image>https://mightycreak.github.io/diffuse/images/screenshot_v0.5.0.png</image>
|
|
||||||
<caption>Main window: diff between two files</caption>
|
<caption>Main window: diff between two files</caption>
|
||||||
|
<image>https://mightycreak.github.io/diffuse/images/screenshot_v0.7.0.png</image>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
<release version="0.6.0" date="2020-11-29">
|
<release version="0.6.0" date="2020-11-29">
|
||||||
<description>
|
<description>
|
||||||
|
@ -102,7 +96,6 @@
|
||||||
</description>
|
</description>
|
||||||
</release>
|
</release>
|
||||||
</releases>
|
</releases>
|
||||||
|
|
||||||
<developer_name>Romain Failliot</developer_name>
|
<developer_name>Romain Failliot</developer_name>
|
||||||
<update_contact>romain.failliot@foolstep.com</update_contact>
|
<update_contact>romain.failliot@foolstep.com</update_contact>
|
||||||
</component>
|
</component>
|
|
@ -1,10 +1,49 @@
|
||||||
desktop_file = 'diffuse.desktop'
|
pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name())
|
||||||
i18n.merge_file(
|
|
||||||
desktop_file,
|
desktop_file = i18n.merge_file(
|
||||||
input: desktop_file + '.in',
|
input: 'io.github.mightycreak.Diffuse.desktop.in',
|
||||||
output: desktop_file,
|
output: 'io.github.mightycreak.Diffuse.desktop',
|
||||||
|
type: 'desktop',
|
||||||
po_dir: '../po',
|
po_dir: '../po',
|
||||||
install: true,
|
install: true,
|
||||||
install_dir: join_paths(datadir, 'applications'),
|
install_dir: join_paths(get_option('datadir'), 'applications')
|
||||||
type: 'desktop'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
desktop_utils = find_program('desktop-file-validate', required: false)
|
||||||
|
if desktop_utils.found()
|
||||||
|
test('Validate desktop file', desktop_utils,
|
||||||
|
args: [desktop_file]
|
||||||
|
)
|
||||||
|
endif
|
||||||
|
|
||||||
|
appstream_file = i18n.merge_file(
|
||||||
|
input: 'io.github.mightycreak.Diffuse.metainfo.xml.in',
|
||||||
|
output: 'io.github.mightycreak.Diffuse.metainfo.xml',
|
||||||
|
po_dir: '../po',
|
||||||
|
install: true,
|
||||||
|
install_dir: join_paths(get_option('datadir'), 'appdata')
|
||||||
|
)
|
||||||
|
|
||||||
|
appstream_util = find_program('appstream-util', required: false)
|
||||||
|
if appstream_util.found()
|
||||||
|
test('Validate appstream file', appstream_util,
|
||||||
|
args: ['validate', appstream_file]
|
||||||
|
)
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Diffuse config file
|
||||||
|
conf = configuration_data()
|
||||||
|
conf.set('PKGDATADIR', pkgdatadir)
|
||||||
|
|
||||||
|
configure_file(
|
||||||
|
input: 'diffuserc.in',
|
||||||
|
output: 'diffuserc',
|
||||||
|
configuration: conf,
|
||||||
|
install: true,
|
||||||
|
install_dir: get_option('sysconfdir')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Data files
|
||||||
|
install_subdir('usr/share', install_dir: get_option('datadir'), strip_directory: true)
|
||||||
|
|
||||||
|
subdir('icons')
|
||||||
|
|
|
@ -10,9 +10,14 @@ finish-args:
|
||||||
- --filesystem=home
|
- --filesystem=home
|
||||||
modules:
|
modules:
|
||||||
- name: diffuse
|
- name: diffuse
|
||||||
|
builddir: true
|
||||||
buildsystem: meson
|
buildsystem: meson
|
||||||
sources:
|
sources:
|
||||||
- type: git
|
- type: dir
|
||||||
url: https://github.com/MightyCreak/diffuse
|
path: .
|
||||||
branch: v0.6.0
|
# - type: git
|
||||||
rename-desktop-file: diffuse.desktop
|
# url: file:///home/creak/dev/diffuse
|
||||||
|
# branch: split-code-into-modules
|
||||||
|
# - type: git
|
||||||
|
# url: https://github.com/MightyCreak/diffuse
|
||||||
|
# branch: v0.6.0
|
||||||
|
|
26
meson.build
26
meson.build
|
@ -1,29 +1,13 @@
|
||||||
project('diffuse',
|
project('diffuse',
|
||||||
version: '0.6.0',
|
version: '0.7.0',
|
||||||
meson_version: '>= 0.50',
|
meson_version: '>= 0.50',
|
||||||
license: 'GPL-2.0-or-later')
|
license: 'GPL-2.0-or-later',
|
||||||
|
default_options: [ 'warning_level=2' ])
|
||||||
|
|
||||||
i18n = import('i18n')
|
i18n = import('i18n')
|
||||||
python = import('python')
|
|
||||||
|
|
||||||
py_installation = python.find_installation('python3')
|
|
||||||
|
|
||||||
find_program('gtk-update-icon-cache', required: false)
|
|
||||||
|
|
||||||
prefix = get_option('prefix')
|
|
||||||
|
|
||||||
bindir = prefix / get_option('bindir')
|
|
||||||
datadir = prefix / get_option('datadir')
|
|
||||||
localedir = prefix / get_option('localedir')
|
|
||||||
libexecdir = prefix / get_option('libexecdir')
|
|
||||||
sysconfdir = prefix / get_option('sysconfdir')
|
|
||||||
pythondir = py_installation.get_path('purelib')
|
|
||||||
|
|
||||||
pkgdatadir = join_paths(datadir, meson.project_name())
|
|
||||||
podir = join_paths(meson.source_root(), 'po')
|
|
||||||
|
|
||||||
subdir('po')
|
|
||||||
subdir('data')
|
subdir('data')
|
||||||
subdir('src')
|
subdir('src')
|
||||||
|
subdir('po')
|
||||||
|
|
||||||
meson.add_install_script('build-scripts/meson-postinstall.py')
|
meson.add_install_script('build-aux/meson/postinstall.py')
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
#!@PYTHON@
|
||||||
|
|
||||||
|
# Diffuse: a graphical tool for merging and comparing text files.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2019 Derrick Moser <derrick_moser@yahoo.com>
|
||||||
|
# Copyright (C) 2021 Romain Failliot <romain.failliot@foolstep.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 2 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, write to the Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import gettext
|
||||||
|
import locale
|
||||||
|
|
||||||
|
VERSION = '@VERSION@'
|
||||||
|
pkgdatadir = '@pkgdatadir@'
|
||||||
|
localedir = '@localedir@'
|
||||||
|
sysconfigdir = '@sysconfigdir@'
|
||||||
|
|
||||||
|
sys.path.insert(1, pkgdatadir)
|
||||||
|
gettext.install('diffuse', localedir)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from diffuse import main
|
||||||
|
|
||||||
|
sys.exit(main.main(VERSION, sysconfigdir))
|
|
@ -1,121 +1,34 @@
|
||||||
#!/usr/bin/env python3
|
# Diffuse: a graphical tool for merging and comparing text files.
|
||||||
# -*- coding: utf-8 -*-
|
#
|
||||||
|
# Copyright (C) 2019 Derrick Moser <derrick_moser@yahoo.com>
|
||||||
|
# Copyright (C) 2021 Romain Failliot <romain.failliot@foolstep.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 2 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, write to the Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
# Copyright (C) 2006-2019 Derrick Moser <derrick_moser@yahoo.com>
|
|
||||||
# Copyright (C) 2015-2020 Romain Failliot <romain.failliot@foolstep.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 2 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. You may also obtain a copy of the GNU General Public License
|
|
||||||
# from the Free Software Foundation by visiting their web site
|
|
||||||
# (http://www.fsf.org/) or by writing to the Free Software Foundation, Inc.,
|
|
||||||
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
|
|
||||||
import codecs
|
|
||||||
import gettext
|
|
||||||
import locale
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import codecs
|
||||||
# use the program's location as a starting place to search for supporting files
|
import difflib
|
||||||
# such as icon and help documentation
|
import encodings
|
||||||
if hasattr(sys, 'frozen'):
|
import glob
|
||||||
app_path = sys.executable
|
import re
|
||||||
else:
|
import shlex
|
||||||
app_path = os.path.realpath(sys.argv[0])
|
import stat
|
||||||
bin_dir = os.path.dirname(app_path)
|
import subprocess
|
||||||
|
import unicodedata
|
||||||
# platform test
|
import webbrowser
|
||||||
def isWindows():
|
|
||||||
return os.name == 'nt'
|
|
||||||
|
|
||||||
# translation location: '../share/locale/<LANG>/LC_MESSAGES/diffuse.mo'
|
|
||||||
# where '<LANG>' is the language key
|
|
||||||
lang = locale.getdefaultlocale()[0]
|
|
||||||
if isWindows():
|
|
||||||
# gettext looks for the language using environment variables which
|
|
||||||
# are normally not set on Windows so we try setting it for them
|
|
||||||
for v in 'LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG':
|
|
||||||
if v in os.environ:
|
|
||||||
lang = os.environ[v]
|
|
||||||
# remove any additional languages, encodings, or modifications
|
|
||||||
for v in ':.@':
|
|
||||||
lang = lang.split(v)[0]
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
if lang is not None:
|
|
||||||
os.environ['LANG'] = lang
|
|
||||||
del v
|
|
||||||
locale_dir = 'locale'
|
|
||||||
else:
|
|
||||||
locale_dir = '../share/locale'
|
|
||||||
locale_dir = os.path.join(bin_dir, locale_dir)
|
|
||||||
gettext.bindtextdomain('diffuse', locale_dir)
|
|
||||||
|
|
||||||
gettext.textdomain('diffuse')
|
|
||||||
_ = gettext.gettext
|
|
||||||
|
|
||||||
APP_NAME = 'Diffuse'
|
|
||||||
VERSION = '0.6.0'
|
|
||||||
COPYRIGHT = '''{copyright} © 2006-2019 Derrick Moser
|
|
||||||
{copyright} © 2015-2020 Romain Failliot'''.format(copyright=_("Copyright"))
|
|
||||||
WEBSITE = 'https://github.com/MightyCreak/diffuse'
|
|
||||||
|
|
||||||
# process help options
|
|
||||||
if __name__ == '__main__':
|
|
||||||
args = sys.argv
|
|
||||||
argc = len(args)
|
|
||||||
if argc == 2 and args[1] in [ '-v', '--version' ]:
|
|
||||||
print(f'{APP_NAME} {VERSION}\n{COPYRIGHT}')
|
|
||||||
sys.exit(0)
|
|
||||||
if argc == 2 and args[1] in [ '-h', '-?', '--help' ]:
|
|
||||||
print(_('''Usage:
|
|
||||||
diffuse [ [OPTION...] [FILE...] ]...
|
|
||||||
diffuse ( -h | -? | --help | -v | --version )
|
|
||||||
|
|
||||||
Diffuse is a graphical tool for merging and comparing text files. Diffuse is
|
|
||||||
able to compare an arbitrary number of files side-by-side and gives users the
|
|
||||||
ability to manually adjust line matching and directly edit files. Diffuse can
|
|
||||||
also retrieve revisions of files from Bazaar, CVS, Darcs, Git, Mercurial,
|
|
||||||
Monotone, RCS, Subversion, and SVK repositories for comparison and merging.
|
|
||||||
|
|
||||||
Help Options:
|
|
||||||
( -h | -? | --help ) Display this usage information
|
|
||||||
( -v | --version ) Display version and copyright information
|
|
||||||
|
|
||||||
Configuration Options:
|
|
||||||
--no-rcfile Do not read any resource files
|
|
||||||
--rcfile <file> Specify explicit resource file
|
|
||||||
|
|
||||||
General Options:
|
|
||||||
( -c | --commit ) <rev> File revisions <rev-1> and <rev>
|
|
||||||
( -D | --close-if-same ) Close all tabs with no differences
|
|
||||||
( -e | --encoding ) <codec> Use <codec> to read and write files
|
|
||||||
( -L | --label ) <label> Display <label> instead of the file name
|
|
||||||
( -m | --modified ) Create a new tab for each modified file
|
|
||||||
( -r | --revision ) <rev> File revision <rev>
|
|
||||||
( -s | --separate ) Create a new tab for each file
|
|
||||||
( -t | --tab ) Start a new tab
|
|
||||||
( -V | --vcs ) <vcs-list> Version control system search order
|
|
||||||
--line <line> Start with line <line> selected
|
|
||||||
--null-file Create a blank file comparison pane
|
|
||||||
|
|
||||||
Display Options:
|
|
||||||
( -b | --ignore-space-change ) Ignore changes to white space
|
|
||||||
( -B | --ignore-blank-lines ) Ignore changes in blank lines
|
|
||||||
( -E | --ignore-end-of-line ) Ignore end of line differences
|
|
||||||
( -i | --ignore-case ) Ignore case differences
|
|
||||||
( -w | --ignore-all-space ) Ignore white space differences'''))
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
|
@ -137,27 +50,14 @@ from gi.repository import Pango
|
||||||
gi.require_version('PangoCairo', '1.0')
|
gi.require_version('PangoCairo', '1.0')
|
||||||
from gi.repository import PangoCairo
|
from gi.repository import PangoCairo
|
||||||
|
|
||||||
import difflib
|
|
||||||
import encodings
|
|
||||||
import glob
|
|
||||||
import re
|
|
||||||
import shlex
|
|
||||||
import stat
|
|
||||||
import string
|
|
||||||
import subprocess
|
|
||||||
import unicodedata
|
|
||||||
import webbrowser
|
|
||||||
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from diffuse import utils
|
||||||
|
|
||||||
if not hasattr(__builtins__, 'WindowsError'):
|
if not hasattr(__builtins__, 'WindowsError'):
|
||||||
# define 'WindowsError' so 'except' statements will work on all platforms
|
# define 'WindowsError' so 'except' statements will work on all platforms
|
||||||
WindowsError = IOError
|
WindowsError = IOError
|
||||||
|
|
||||||
# convenience function to display debug messages
|
|
||||||
def logDebug(s):
|
|
||||||
pass #sys.stderr.write(f'{APP_NAME}: {s}\n')
|
|
||||||
|
|
||||||
# avoid some dictionary lookups when string.whitespace is used in loops
|
# avoid some dictionary lookups when string.whitespace is used in loops
|
||||||
# this is sorted based upon frequency to speed up code for stripping whitespace
|
# this is sorted based upon frequency to speed up code for stripping whitespace
|
||||||
whitespace = ' \t\n\r\x0b\x0c'
|
whitespace = ' \t\n\r\x0b\x0c'
|
||||||
|
@ -167,27 +67,6 @@ def globEscape(s):
|
||||||
m = dict([ (c, f'[{c}]') for c in '[]?*' ])
|
m = dict([ (c, f'[{c}]') for c in '[]?*' ])
|
||||||
return ''.join([ m.get(c, c) for c in s ])
|
return ''.join([ m.get(c, c) for c in s ])
|
||||||
|
|
||||||
# associate our icon with all of our windows
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# this is not automatically set on some older version of PyGTK
|
|
||||||
Gtk.Window.set_default_icon_name('diffuse')
|
|
||||||
|
|
||||||
# convenience class for displaying a message dialogue
|
|
||||||
class MessageDialog(Gtk.MessageDialog):
|
|
||||||
def __init__(self, parent, type, s):
|
|
||||||
if type == Gtk.MessageType.ERROR:
|
|
||||||
buttons = Gtk.ButtonsType.OK
|
|
||||||
else:
|
|
||||||
buttons = Gtk.ButtonsType.OK_CANCEL
|
|
||||||
Gtk.MessageDialog.__init__(self, parent = parent, destroy_with_parent = True, message_type = type, buttons = buttons, text = s)
|
|
||||||
self.set_title(APP_NAME)
|
|
||||||
|
|
||||||
# report error messages
|
|
||||||
def logError(s):
|
|
||||||
m = MessageDialog(None, Gtk.MessageType.ERROR, s)
|
|
||||||
m.run()
|
|
||||||
m.destroy()
|
|
||||||
|
|
||||||
# colour resources
|
# colour resources
|
||||||
class Colour:
|
class Colour:
|
||||||
def __init__(self, r, g, b, a=1.0):
|
def __init__(self, r, g, b, a=1.0):
|
||||||
|
@ -544,7 +423,7 @@ class Resources:
|
||||||
try:
|
try:
|
||||||
return self.colours[symbol]
|
return self.colours[symbol]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
logDebug(f'Warning: unknown colour "{symbol}"')
|
utils.logDebug(f'Warning: unknown colour "{symbol}"')
|
||||||
self.colours[symbol] = v = Colour(0.0, 0.0, 0.0)
|
self.colours[symbol] = v = Colour(0.0, 0.0, 0.0)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
@ -553,7 +432,7 @@ class Resources:
|
||||||
try:
|
try:
|
||||||
return self.floats[symbol]
|
return self.floats[symbol]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
logDebug(f'Warning: unknown float "{symbol}"')
|
utils.logDebug(f'Warning: unknown float "{symbol}"')
|
||||||
self.floats[symbol] = v = 0.5
|
self.floats[symbol] = v = 0.5
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
@ -562,7 +441,7 @@ class Resources:
|
||||||
try:
|
try:
|
||||||
return self.strings[symbol]
|
return self.strings[symbol]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
logDebug(f'Warning: unknown string "{symbol}"')
|
utils.logDebug(f'Warning: unknown string "{symbol}"')
|
||||||
self.strings[symbol] = v = ''
|
self.strings[symbol] = v = ''
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
@ -687,7 +566,7 @@ class Resources:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
flags = 0
|
flags = 0
|
||||||
if isWindows():
|
if utils.isWindows():
|
||||||
flags |= re.IGNORECASE
|
flags |= re.IGNORECASE
|
||||||
self.syntax_file_patterns[key] = re.compile(args[2], flags)
|
self.syntax_file_patterns[key] = re.compile(args[2], flags)
|
||||||
# eg. default to the Python syntax rules when viewing
|
# eg. default to the Python syntax rules when viewing
|
||||||
|
@ -713,7 +592,7 @@ class Resources:
|
||||||
raise ValueError()
|
raise ValueError()
|
||||||
except: # Grr... the 're' module throws weird errors
|
except: # Grr... the 're' module throws weird errors
|
||||||
#except ValueError:
|
#except ValueError:
|
||||||
logError(_('Error processing line %(line)d of %(file)s.') % { 'line': i + 1, 'file': file_name })
|
utils.logError(_('Error processing line %(line)d of %(file)s.') % { 'line': i + 1, 'file': file_name })
|
||||||
|
|
||||||
theResources = Resources()
|
theResources = Resources()
|
||||||
|
|
||||||
|
@ -799,7 +678,7 @@ class Preferences:
|
||||||
# find available encodings
|
# find available encodings
|
||||||
self.encodings = sorted(set(encodings.aliases.aliases.values()))
|
self.encodings = sorted(set(encodings.aliases.aliases.values()))
|
||||||
|
|
||||||
if isWindows():
|
if utils.isWindows():
|
||||||
svk_bin = 'svk.bat'
|
svk_bin = 'svk.bat'
|
||||||
else:
|
else:
|
||||||
svk_bin = 'svk'
|
svk_bin = 'svk'
|
||||||
|
@ -860,7 +739,7 @@ class Preferences:
|
||||||
[ 'List',
|
[ 'List',
|
||||||
[ 'Integer', 'tabs_default_panes', 2, _('Default panes'), 2, 16 ],
|
[ 'Integer', 'tabs_default_panes', 2, _('Default panes'), 2, 16 ],
|
||||||
[ 'Boolean', 'tabs_always_show', False, _('Always show the tab bar') ],
|
[ 'Boolean', 'tabs_always_show', False, _('Always show the tab bar') ],
|
||||||
[ 'Boolean', 'tabs_warn_before_quit', True, _('Warn me when closing a tab will quit %s') % APP_NAME ]
|
[ 'Boolean', 'tabs_warn_before_quit', True, _('Warn me when closing a tab will quit %s') % utils.APP_NAME ]
|
||||||
],
|
],
|
||||||
_('Regional Settings'),
|
_('Regional Settings'),
|
||||||
[ 'List',
|
[ 'List',
|
||||||
|
@ -878,7 +757,7 @@ class Preferences:
|
||||||
'align_ignore_blanklines': ('align_ignore_whitespace', True),
|
'align_ignore_blanklines': ('align_ignore_whitespace', True),
|
||||||
'align_ignore_endofline': ('align_ignore_whitespace', True)
|
'align_ignore_endofline': ('align_ignore_whitespace', True)
|
||||||
}
|
}
|
||||||
if isWindows():
|
if utils.isWindows():
|
||||||
root = os.environ.get('SYSTEMDRIVE', None)
|
root = os.environ.get('SYSTEMDRIVE', None)
|
||||||
if root is None:
|
if root is None:
|
||||||
root = 'C:\\'
|
root = 'C:\\'
|
||||||
|
@ -914,7 +793,7 @@ class Preferences:
|
||||||
[ 'File', key + '_bin_rlog', 'rlog', _('"rlog" command') ] ])
|
[ 'File', key + '_bin_rlog', 'rlog', _('"rlog" command') ] ])
|
||||||
else:
|
else:
|
||||||
temp.extend([ [ 'File', key + '_bin', cmd, _('Command') ] ])
|
temp.extend([ [ 'File', key + '_bin', cmd, _('Command') ] ])
|
||||||
if isWindows():
|
if utils.isWindows():
|
||||||
temp.append([ 'Boolean', key + '_bash', False, _('Launch from a Bash login shell') ])
|
temp.append([ 'Boolean', key + '_bash', False, _('Launch from a Bash login shell') ])
|
||||||
if key != 'git':
|
if key != 'git':
|
||||||
temp.append([ 'Boolean', key + '_cygwin', False, _('Update paths for Cygwin') ])
|
temp.append([ 'Boolean', key + '_cygwin', False, _('Update paths for Cygwin') ])
|
||||||
|
@ -949,10 +828,10 @@ class Preferences:
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# this may happen if the prefs were written by a
|
# this may happen if the prefs were written by a
|
||||||
# different version -- don't bother the user
|
# different version -- don't bother the user
|
||||||
logDebug(f'Error processing line {j + 1} of {self.path}.')
|
utils.logDebug(f'Error processing line {j + 1} of {self.path}.')
|
||||||
except IOError:
|
except IOError:
|
||||||
# bad $HOME value? -- don't bother the user
|
# bad $HOME value? -- don't bother the user
|
||||||
logDebug(f'Error reading {self.path}.')
|
utils.logDebug(f'Error reading {self.path}.')
|
||||||
|
|
||||||
# recursively traverses 'template' to discover the preferences and
|
# recursively traverses 'template' to discover the preferences and
|
||||||
# initialise their default values in self.bool_prefs, self.int_prefs, and
|
# initialise their default values in self.bool_prefs, self.int_prefs, and
|
||||||
|
@ -1021,12 +900,12 @@ class Preferences:
|
||||||
ss.append(f'{k} "{v_escaped}"\n')
|
ss.append(f'{k} "{v_escaped}"\n')
|
||||||
ss.sort()
|
ss.sort()
|
||||||
f = open(self.path, 'w')
|
f = open(self.path, 'w')
|
||||||
f.write(f'# This prefs file was generated by {APP_NAME} {VERSION}.\n\n')
|
f.write(f'# This prefs file was generated by {utils.APP_NAME} {utils.VERSION}.\n\n')
|
||||||
for s in ss:
|
for s in ss:
|
||||||
f.write(s)
|
f.write(s)
|
||||||
f.close()
|
f.close()
|
||||||
except IOError:
|
except IOError:
|
||||||
m = MessageDialog(parent, Gtk.MessageType.ERROR, _('Error writing %s.') % (self.path, ))
|
m = utils.MessageDialog(parent, Gtk.MessageType.ERROR, _('Error writing %s.') % (self.path, ))
|
||||||
m.run()
|
m.run()
|
||||||
m.destroy()
|
m.destroy()
|
||||||
dialog.destroy()
|
dialog.destroy()
|
||||||
|
@ -1143,7 +1022,7 @@ class Preferences:
|
||||||
# cygwin and native applications can be used on windows, use this method
|
# cygwin and native applications can be used on windows, use this method
|
||||||
# to convert a path to the usual form expected on sys.platform
|
# to convert a path to the usual form expected on sys.platform
|
||||||
def convertToNativePath(self, s):
|
def convertToNativePath(self, s):
|
||||||
if isWindows() and s.find('/') >= 0:
|
if utils.isWindows() and s.find('/') >= 0:
|
||||||
# treat as a cygwin path
|
# treat as a cygwin path
|
||||||
s = s.replace(os.sep, '/')
|
s = s.replace(os.sep, '/')
|
||||||
# convert to a Windows native style path
|
# convert to a Windows native style path
|
||||||
|
@ -1319,7 +1198,7 @@ def drive_from_path(s):
|
||||||
|
|
||||||
# constructs a relative path from 'a' to 'b', both should be absolute paths
|
# constructs a relative path from 'a' to 'b', both should be absolute paths
|
||||||
def relpath(a, b):
|
def relpath(a, b):
|
||||||
if isWindows():
|
if utils.isWindows():
|
||||||
if drive_from_path(a) != drive_from_path(b):
|
if drive_from_path(a) != drive_from_path(b):
|
||||||
return b
|
return b
|
||||||
c1 = [ c for c in a.split(os.sep) if c != '' ]
|
c1 = [ c for c in a.split(os.sep) if c != '' ]
|
||||||
|
@ -1335,7 +1214,7 @@ def relpath(a, b):
|
||||||
# by prepending './' to the basename
|
# by prepending './' to the basename
|
||||||
def safeRelativePath(abspath1, name, prefs, cygwin_pref):
|
def safeRelativePath(abspath1, name, prefs, cygwin_pref):
|
||||||
s = os.path.join(os.curdir, relpath(abspath1, os.path.abspath(name)))
|
s = os.path.join(os.curdir, relpath(abspath1, os.path.abspath(name)))
|
||||||
if isWindows():
|
if utils.isWindows():
|
||||||
if prefs.getBool(cygwin_pref):
|
if prefs.getBool(cygwin_pref):
|
||||||
s = s.replace('\\', '/')
|
s = s.replace('\\', '/')
|
||||||
else:
|
else:
|
||||||
|
@ -1350,12 +1229,12 @@ def bashEscape(s):
|
||||||
def popenRead(dn, cmd, prefs, bash_pref, success_results=None):
|
def popenRead(dn, cmd, prefs, bash_pref, success_results=None):
|
||||||
if success_results is None:
|
if success_results is None:
|
||||||
success_results = [ 0 ]
|
success_results = [ 0 ]
|
||||||
if isWindows() and prefs.getBool(bash_pref):
|
if utils.isWindows() and prefs.getBool(bash_pref):
|
||||||
# launch the command from a bash shell is requested
|
# launch the command from a bash shell is requested
|
||||||
cmd = [ prefs.convertToNativePath('/bin/bash.exe'), '-l', '-c', 'cd {}; {}'.format(bashEscape(dn), ' '.join([ bashEscape(arg) for arg in cmd ])) ]
|
cmd = [ prefs.convertToNativePath('/bin/bash.exe'), '-l', '-c', 'cd {}; {}'.format(bashEscape(dn), ' '.join([ bashEscape(arg) for arg in cmd ])) ]
|
||||||
dn = None
|
dn = None
|
||||||
# use subprocess.Popen to retrieve the file contents
|
# use subprocess.Popen to retrieve the file contents
|
||||||
if isWindows():
|
if utils.isWindows():
|
||||||
info = subprocess.STARTUPINFO()
|
info = subprocess.STARTUPINFO()
|
||||||
info.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
info.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
info.wShowWindow = subprocess.SW_HIDE
|
info.wShowWindow = subprocess.SW_HIDE
|
||||||
|
@ -1653,7 +1532,7 @@ class _Cvs:
|
||||||
k0 = k
|
k0 = k
|
||||||
result.append([ (k0, prev), (k, rev) ])
|
result.append([ (k0, prev), (k, rev) ])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logError(_('Error parsing revision %s.') % (rev, ))
|
utils.logError(_('Error parsing revision %s.') % (rev, ))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def getFolderTemplate(self, prefs, names):
|
def getFolderTemplate(self, prefs, names):
|
||||||
|
@ -2270,7 +2149,7 @@ class _Rcs:
|
||||||
k0 = k
|
k0 = k
|
||||||
result.append([ (k0, prev), (k, rev) ])
|
result.append([ (k0, prev), (k, rev) ])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logError(_('Error parsing revision %s.') % (rev, ))
|
utils.logError(_('Error parsing revision %s.') % (rev, ))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def getFolderTemplate(self, prefs, names):
|
def getFolderTemplate(self, prefs, names):
|
||||||
|
@ -2414,7 +2293,7 @@ class _Svn:
|
||||||
try:
|
try:
|
||||||
prev = self._getPreviousRevision(rev)
|
prev = self._getPreviousRevision(rev)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logError(_('Error parsing revision %s.') % (rev, ))
|
utils.logError(_('Error parsing revision %s.') % (rev, ))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# build command
|
# build command
|
||||||
|
@ -2599,7 +2478,7 @@ def _get_svk_repo(path, prefs):
|
||||||
name = path
|
name = path
|
||||||
# parse the ~/.svk/config file to discover which directories are part of
|
# parse the ~/.svk/config file to discover which directories are part of
|
||||||
# SVK repositories
|
# SVK repositories
|
||||||
if isWindows():
|
if utils.isWindows():
|
||||||
name = name.upper()
|
name = name.upper()
|
||||||
svkroot = os.environ.get('SVKROOT', None)
|
svkroot = os.environ.get('SVKROOT', None)
|
||||||
if svkroot is None:
|
if svkroot is None:
|
||||||
|
@ -2647,7 +2526,7 @@ def _get_svk_repo(path, prefs):
|
||||||
tt.append(key[j])
|
tt.append(key[j])
|
||||||
j += 1
|
j += 1
|
||||||
key = ''.join(tt).replace(sep, os.sep)
|
key = ''.join(tt).replace(sep, os.sep)
|
||||||
if isWindows():
|
if utils.isWindows():
|
||||||
key = key.upper()
|
key = key.upper()
|
||||||
projs.append(key)
|
projs.append(key)
|
||||||
break
|
break
|
||||||
|
@ -2655,7 +2534,7 @@ def _get_svk_repo(path, prefs):
|
||||||
if _VcsFolderSet(projs).contains(name):
|
if _VcsFolderSet(projs).contains(name):
|
||||||
return _Svk(path)
|
return _Svk(path)
|
||||||
except IOError:
|
except IOError:
|
||||||
logError(_('Error parsing %s.') % (svkconfig, ))
|
utils.logError(_('Error parsing %s.') % (svkconfig, ))
|
||||||
|
|
||||||
class VCSs:
|
class VCSs:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -2912,7 +2791,7 @@ def path2url(path, proto='file'):
|
||||||
for c in s[i:]:
|
for c in s[i:]:
|
||||||
if c == os.sep:
|
if c == os.sep:
|
||||||
c = '/'
|
c = '/'
|
||||||
elif c == ':' and isWindows():
|
elif c == ':' and utils.isWindows():
|
||||||
c = '|'
|
c = '|'
|
||||||
else:
|
else:
|
||||||
v = ord(c)
|
v = ord(c)
|
||||||
|
@ -6669,7 +6548,7 @@ class SearchDialog(Gtk.Dialog):
|
||||||
|
|
||||||
# convenience method to request confirmation when closing the last tab
|
# convenience method to request confirmation when closing the last tab
|
||||||
def confirmTabClose(parent):
|
def confirmTabClose(parent):
|
||||||
dialog = MessageDialog(parent, Gtk.MessageType.WARNING, _('Closing this tab will quit %s.') % (APP_NAME, ))
|
dialog = utils.MessageDialog(parent, Gtk.MessageType.WARNING, _('Closing this tab will quit %s.') % (utils.APP_NAME, ))
|
||||||
end = (dialog.run() == Gtk.ResponseType.OK)
|
end = (dialog.run() == Gtk.ResponseType.OK)
|
||||||
dialog.destroy()
|
dialog.destroy()
|
||||||
return end
|
return end
|
||||||
|
@ -6756,31 +6635,38 @@ class NumericDialog(Gtk.Dialog):
|
||||||
def url_hook(dialog, link, userdata):
|
def url_hook(dialog, link, userdata):
|
||||||
webbrowser.open(link)
|
webbrowser.open(link)
|
||||||
|
|
||||||
# the about dialogue
|
|
||||||
|
|
||||||
|
# the about dialog
|
||||||
class AboutDialog(Gtk.AboutDialog):
|
class AboutDialog(Gtk.AboutDialog):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
Gtk.AboutDialog.__init__(self)
|
Gtk.AboutDialog.__init__(self)
|
||||||
self.set_logo_icon_name('diffuse')
|
self.set_logo_icon_name('io.github.mightycreak.Diffuse')
|
||||||
if hasattr(self, 'set_program_name'):
|
self.set_program_name(utils.APP_NAME)
|
||||||
# only available in pygtk >= 2.12
|
self.set_version(utils.VERSION)
|
||||||
self.set_program_name(APP_NAME)
|
|
||||||
self.set_version(VERSION)
|
|
||||||
self.set_comments(_('Diffuse is a graphical tool for merging and comparing text files.'))
|
self.set_comments(_('Diffuse is a graphical tool for merging and comparing text files.'))
|
||||||
self.set_copyright(COPYRIGHT)
|
self.set_copyright(utils.COPYRIGHT)
|
||||||
self.set_website(WEBSITE)
|
self.set_website(utils.WEBSITE)
|
||||||
self.set_authors([ 'Derrick Moser <derrick_moser@yahoo.com>',
|
self.set_authors([ 'Derrick Moser <derrick_moser@yahoo.com>',
|
||||||
'Romain Failliot <romain.failliot@foolstep.com>' ])
|
'Romain Failliot <romain.failliot@foolstep.com>' ])
|
||||||
self.set_translator_credits(_('translator-credits'))
|
self.set_translator_credits(_('translator-credits'))
|
||||||
ss = [ APP_NAME + ' ' + VERSION + '\n',
|
license_text = [
|
||||||
COPYRIGHT + '\n\n',
|
utils.APP_NAME + ' ' + utils.VERSION + '\n\n',
|
||||||
_("""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 2 of the licence, or (at your option) any later version.
|
utils.COPYRIGHT + '\n\n',
|
||||||
|
_('''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 2 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.
|
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. You may also obtain a copy of the GNU General Public License from the Free Software Foundation by visiting their web site (http://www.fsf.org/) or by writing to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
You should have received a copy of the GNU General Public License along
|
||||||
""") ]
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
self.set_license(''.join(ss))
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.''') ]
|
||||||
self.set_wrap_license(True)
|
self.set_license(''.join(license_text))
|
||||||
|
|
||||||
# widget classed to create notebook tabs with labels and a close button
|
# widget classed to create notebook tabs with labels and a close button
|
||||||
# use notebooktab.button.connect() to be notified when the button is pressed
|
# use notebooktab.button.connect() to be notified when the button is pressed
|
||||||
|
@ -7012,7 +6898,7 @@ class Diffuse(Gtk.Window):
|
||||||
if self.headers[f].has_edits:
|
if self.headers[f].has_edits:
|
||||||
# warn users of any unsaved changes they might lose
|
# warn users of any unsaved changes they might lose
|
||||||
dialog = Gtk.MessageDialog(self.get_toplevel(), Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.WARNING, Gtk.ButtonsType.NONE, _('Save changes before loading the new file?'))
|
dialog = Gtk.MessageDialog(self.get_toplevel(), Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.WARNING, Gtk.ButtonsType.NONE, _('Save changes before loading the new file?'))
|
||||||
dialog.set_title(APP_NAME)
|
dialog.set_title(utils.APP_NAME)
|
||||||
dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
||||||
dialog.add_button(Gtk.STOCK_NO, Gtk.ResponseType.REJECT)
|
dialog.add_button(Gtk.STOCK_NO, Gtk.ResponseType.REJECT)
|
||||||
dialog.add_button(Gtk.STOCK_YES, Gtk.ResponseType.OK)
|
dialog.add_button(Gtk.STOCK_YES, Gtk.ResponseType.OK)
|
||||||
|
@ -7124,7 +7010,7 @@ class Diffuse(Gtk.Window):
|
||||||
msg = _('Error reading revision %(rev)s of %(file)s.') % { 'rev': rev, 'file': name }
|
msg = _('Error reading revision %(rev)s of %(file)s.') % { 'rev': rev, 'file': name }
|
||||||
else:
|
else:
|
||||||
msg = _('Error reading %s.') % (name, )
|
msg = _('Error reading %s.') % (name, )
|
||||||
dialog = MessageDialog(self.get_toplevel(), Gtk.MessageType.ERROR, msg)
|
dialog = utils.MessageDialog(self.get_toplevel(), Gtk.MessageType.ERROR, msg)
|
||||||
dialog.run()
|
dialog.run()
|
||||||
dialog.destroy()
|
dialog.destroy()
|
||||||
return
|
return
|
||||||
|
@ -7197,7 +7083,7 @@ class Diffuse(Gtk.Window):
|
||||||
else:
|
else:
|
||||||
s = info.name
|
s = info.name
|
||||||
msg = _('The file %s changed on disk. Do you want to reload the file?') % (s, )
|
msg = _('The file %s changed on disk. Do you want to reload the file?') % (s, )
|
||||||
dialog = MessageDialog(self.get_toplevel(), Gtk.MessageType.QUESTION, msg)
|
dialog = utils.MessageDialog(self.get_toplevel(), Gtk.MessageType.QUESTION, msg)
|
||||||
ok = (dialog.run() == Gtk.ResponseType.OK)
|
ok = (dialog.run() == Gtk.ResponseType.OK)
|
||||||
dialog.destroy()
|
dialog.destroy()
|
||||||
if ok:
|
if ok:
|
||||||
|
@ -7249,7 +7135,7 @@ class Diffuse(Gtk.Window):
|
||||||
if info.stat[stat.ST_MTIME] < os.stat(name)[stat.ST_MTIME]:
|
if info.stat[stat.ST_MTIME] < os.stat(name)[stat.ST_MTIME]:
|
||||||
msg = _('The file %s has been modified by another process since reading it. If you save, all the external changes could be lost. Save anyways?') % (name, )
|
msg = _('The file %s has been modified by another process since reading it. If you save, all the external changes could be lost. Save anyways?') % (name, )
|
||||||
if msg is not None:
|
if msg is not None:
|
||||||
dialog = MessageDialog(self.get_toplevel(), Gtk.MessageType.QUESTION, msg)
|
dialog = utils.MessageDialog(self.get_toplevel(), Gtk.MessageType.QUESTION, msg)
|
||||||
end = (dialog.run() != Gtk.ResponseType.OK)
|
end = (dialog.run() != Gtk.ResponseType.OK)
|
||||||
dialog.destroy()
|
dialog.destroy()
|
||||||
if end:
|
if end:
|
||||||
|
@ -7288,11 +7174,11 @@ class Diffuse(Gtk.Window):
|
||||||
self.setSyntax(syntax)
|
self.setSyntax(syntax)
|
||||||
return True
|
return True
|
||||||
except (UnicodeEncodeError, LookupError):
|
except (UnicodeEncodeError, LookupError):
|
||||||
dialog = MessageDialog(self.get_toplevel(), Gtk.MessageType.ERROR, _('Error encoding to %s.') % (encoding, ))
|
dialog = utils.MessageDialog(self.get_toplevel(), Gtk.MessageType.ERROR, _('Error encoding to %s.') % (encoding, ))
|
||||||
dialog.run()
|
dialog.run()
|
||||||
dialog.destroy()
|
dialog.destroy()
|
||||||
except IOError:
|
except IOError:
|
||||||
dialog = MessageDialog(self.get_toplevel(), Gtk.MessageType.ERROR, _('Error writing %s.') % (name, ))
|
dialog = utils.MessageDialog(self.get_toplevel(), Gtk.MessageType.ERROR, _('Error writing %s.') % (name, ))
|
||||||
dialog.run()
|
dialog.run()
|
||||||
dialog.destroy()
|
dialog.destroy()
|
||||||
return False
|
return False
|
||||||
|
@ -7558,7 +7444,7 @@ class Diffuse(Gtk.Window):
|
||||||
menuspecs.append([ _('_Help'), [
|
menuspecs.append([ _('_Help'), [
|
||||||
[_('_Help Contents...'), self.help_contents_cb, None, Gtk.STOCK_HELP, 'help_contents'],
|
[_('_Help Contents...'), self.help_contents_cb, None, Gtk.STOCK_HELP, 'help_contents'],
|
||||||
[],
|
[],
|
||||||
[_('_About %s...') % (APP_NAME, ), self.about_cb, None, Gtk.STOCK_ABOUT, 'about'] ] ])
|
[_('_About %s...') % (utils.APP_NAME, ), self.about_cb, None, Gtk.STOCK_ABOUT, 'about'] ] ])
|
||||||
|
|
||||||
# used to disable menu events when switching tabs
|
# used to disable menu events when switching tabs
|
||||||
self.menu_update_depth = 0
|
self.menu_update_depth = 0
|
||||||
|
@ -7655,10 +7541,10 @@ class Diffuse(Gtk.Window):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# this may happen if the state was written by a
|
# this may happen if the state was written by a
|
||||||
# different version -- don't bother the user
|
# different version -- don't bother the user
|
||||||
logDebug(f'Error processing line {j + 1} of {statepath}.')
|
utils.logDebug(f'Error processing line {j + 1} of {statepath}.')
|
||||||
except IOError:
|
except IOError:
|
||||||
# bad $HOME value? -- don't bother the user
|
# bad $HOME value? -- don't bother the user
|
||||||
logDebug(f'Error reading {statepath}.')
|
utils.logDebug(f'Error reading {statepath}.')
|
||||||
|
|
||||||
self.move(self.int_state['window_x'], self.int_state['window_y'])
|
self.move(self.int_state['window_x'], self.int_state['window_y'])
|
||||||
self.resize(self.int_state['window_width'], self.int_state['window_height'])
|
self.resize(self.int_state['window_width'], self.int_state['window_height'])
|
||||||
|
@ -7675,13 +7561,13 @@ class Diffuse(Gtk.Window):
|
||||||
ss.append(f'{k} {v}\n')
|
ss.append(f'{k} {v}\n')
|
||||||
ss.sort()
|
ss.sort()
|
||||||
f = open(statepath, 'w')
|
f = open(statepath, 'w')
|
||||||
f.write(f"# This state file was generated by {APP_NAME} {VERSION}.\n\n")
|
f.write(f"# This state file was generated by {utils.APP_NAME} {utils.VERSION}.\n\n")
|
||||||
for s in ss:
|
for s in ss:
|
||||||
f.write(s)
|
f.write(s)
|
||||||
f.close()
|
f.close()
|
||||||
except IOError:
|
except IOError:
|
||||||
# bad $HOME value? -- don't bother the user
|
# bad $HOME value? -- don't bother the user
|
||||||
logDebug(f'Error writing {statepath}.')
|
utils.logDebug(f'Error writing {statepath}.')
|
||||||
|
|
||||||
# select viewer for a newly selected file in the confirm close dialogue
|
# select viewer for a newly selected file in the confirm close dialogue
|
||||||
def __confirmClose_row_activated_cb(self, tree, path, col, model):
|
def __confirmClose_row_activated_cb(self, tree, path, col, model):
|
||||||
|
@ -7715,7 +7601,7 @@ class Diffuse(Gtk.Window):
|
||||||
buttons=Gtk.ButtonsType.NONE,
|
buttons=Gtk.ButtonsType.NONE,
|
||||||
text=_('Some files have unsaved changes. Select the files to save before closing.'))
|
text=_('Some files have unsaved changes. Select the files to save before closing.'))
|
||||||
dialog.set_resizable(True)
|
dialog.set_resizable(True)
|
||||||
dialog.set_title(APP_NAME)
|
dialog.set_title(utils.APP_NAME)
|
||||||
# add list of files with unsaved changes
|
# add list of files with unsaved changes
|
||||||
sw = Gtk.ScrolledWindow.new()
|
sw = Gtk.ScrolledWindow.new()
|
||||||
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||||
|
@ -7802,7 +7688,7 @@ class Diffuse(Gtk.Window):
|
||||||
# update window's title
|
# update window's title
|
||||||
def updateTitle(self, viewer):
|
def updateTitle(self, viewer):
|
||||||
title = self.notebook.get_tab_label(viewer).get_text()
|
title = self.notebook.get_tab_label(viewer).get_text()
|
||||||
self.set_title(f'{title} - {APP_NAME}')
|
self.set_title(f'{title} - {utils.APP_NAME}')
|
||||||
|
|
||||||
# update the message in the status bar
|
# update the message in the status bar
|
||||||
def setStatus(self, s):
|
def setStatus(self, s):
|
||||||
|
@ -7956,7 +7842,7 @@ class Diffuse(Gtk.Window):
|
||||||
viewer.load(i, FileInfo(name, encoding, vcs, rev))
|
viewer.load(i, FileInfo(name, encoding, vcs, rev))
|
||||||
viewer.setOptions(options)
|
viewer.setOptions(options)
|
||||||
except (IOError, OSError, WindowsError):
|
except (IOError, OSError, WindowsError):
|
||||||
dialog = MessageDialog(self.get_toplevel(), Gtk.MessageType.ERROR, _('Error retrieving commits for %s.') % (dn, ))
|
dialog = utils.MessageDialog(self.get_toplevel(), Gtk.MessageType.ERROR, _('Error retrieving commits for %s.') % (dn, ))
|
||||||
dialog.run()
|
dialog.run()
|
||||||
dialog.destroy()
|
dialog.destroy()
|
||||||
|
|
||||||
|
@ -7987,7 +7873,7 @@ class Diffuse(Gtk.Window):
|
||||||
viewer.load(i, FileInfo(name, encoding, vcs, rev))
|
viewer.load(i, FileInfo(name, encoding, vcs, rev))
|
||||||
viewer.setOptions(options)
|
viewer.setOptions(options)
|
||||||
except (IOError, OSError, WindowsError):
|
except (IOError, OSError, WindowsError):
|
||||||
dialog = MessageDialog(self.get_toplevel(), Gtk.MessageType.ERROR, _('Error retrieving modifications for %s.') % (dn, ))
|
dialog = utils.MessageDialog(self.get_toplevel(), Gtk.MessageType.ERROR, _('Error retrieving modifications for %s.') % (dn, ))
|
||||||
dialog.run()
|
dialog.run()
|
||||||
dialog.destroy()
|
dialog.destroy()
|
||||||
|
|
||||||
|
@ -8048,7 +7934,7 @@ class Diffuse(Gtk.Window):
|
||||||
self.notebook.set_current_page(n)
|
self.notebook.set_current_page(n)
|
||||||
self.getCurrentViewer().grab_focus()
|
self.getCurrentViewer().grab_focus()
|
||||||
else:
|
else:
|
||||||
m = MessageDialog(parent, Gtk.MessageType.ERROR, _('No modified files found.'))
|
m = utils.MessageDialog(parent, Gtk.MessageType.ERROR, _('No modified files found.'))
|
||||||
m.run()
|
m.run()
|
||||||
m.destroy()
|
m.destroy()
|
||||||
|
|
||||||
|
@ -8068,7 +7954,7 @@ class Diffuse(Gtk.Window):
|
||||||
self.notebook.set_current_page(n)
|
self.notebook.set_current_page(n)
|
||||||
self.getCurrentViewer().grab_focus()
|
self.getCurrentViewer().grab_focus()
|
||||||
else:
|
else:
|
||||||
m = MessageDialog(parent, Gtk.MessageType.ERROR, _('No committed files found.'))
|
m = utils.MessageDialog(parent, Gtk.MessageType.ERROR, _('No committed files found.'))
|
||||||
m.run()
|
m.run()
|
||||||
m.destroy()
|
m.destroy()
|
||||||
|
|
||||||
|
@ -8172,7 +8058,7 @@ class Diffuse(Gtk.Window):
|
||||||
msg = _('Phrase not found. Continue from the end of the file?')
|
msg = _('Phrase not found. Continue from the end of the file?')
|
||||||
else:
|
else:
|
||||||
msg = _('Phrase not found. Continue from the start of the file?')
|
msg = _('Phrase not found. Continue from the start of the file?')
|
||||||
dialog = MessageDialog(self.get_toplevel(), Gtk.MessageType.QUESTION, msg)
|
dialog = utils.MessageDialog(self.get_toplevel(), Gtk.MessageType.QUESTION, msg)
|
||||||
dialog.set_default_response(Gtk.ResponseType.OK)
|
dialog.set_default_response(Gtk.ResponseType.OK)
|
||||||
more = (dialog.run() == Gtk.ResponseType.OK)
|
more = (dialog.run() == Gtk.ResponseType.OK)
|
||||||
dialog.destroy()
|
dialog.destroy()
|
||||||
|
@ -8238,15 +8124,15 @@ class Diffuse(Gtk.Window):
|
||||||
# display help documentation
|
# display help documentation
|
||||||
def help_contents_cb(self, widget, data):
|
def help_contents_cb(self, widget, data):
|
||||||
help_url = None
|
help_url = None
|
||||||
if isWindows():
|
if utils.isWindows():
|
||||||
# help documentation is distributed as local HTML files
|
# help documentation is distributed as local HTML files
|
||||||
# search for localised manual first
|
# search for localised manual first
|
||||||
parts = [ 'manual' ]
|
parts = [ 'manual' ]
|
||||||
if lang is not None:
|
if utils.lang is not None:
|
||||||
parts = [ 'manual' ]
|
parts = [ 'manual' ]
|
||||||
parts.extend(lang.split('_'))
|
parts.extend(utils.lang.split('_'))
|
||||||
while len(parts) > 0:
|
while len(parts) > 0:
|
||||||
help_file = os.path.join(bin_dir, '_'.join(parts) + '.html')
|
help_file = os.path.join(utils.bin_dir, '_'.join(parts) + '.html')
|
||||||
if os.path.isfile(help_file):
|
if os.path.isfile(help_file):
|
||||||
# we found a help file
|
# we found a help file
|
||||||
help_url = path2url(help_file)
|
help_url = path2url(help_file)
|
||||||
|
@ -8264,11 +8150,11 @@ class Diffuse(Gtk.Window):
|
||||||
break
|
break
|
||||||
if browser is not None:
|
if browser is not None:
|
||||||
# find localised help file
|
# find localised help file
|
||||||
if lang is None:
|
if utils.lang is None:
|
||||||
parts = []
|
parts = []
|
||||||
else:
|
else:
|
||||||
parts = lang.split('_')
|
parts = utils.lang.split('_')
|
||||||
s = os.path.abspath(os.path.join(bin_dir, '../share/gnome/help/diffuse'))
|
s = os.path.abspath(os.path.join(utils.bin_dir, '../share/gnome/help/diffuse'))
|
||||||
while True:
|
while True:
|
||||||
if len(parts) > 0:
|
if len(parts) > 0:
|
||||||
d = '_'.join(parts)
|
d = '_'.join(parts)
|
||||||
|
@ -8286,10 +8172,10 @@ class Diffuse(Gtk.Window):
|
||||||
del parts[-1]
|
del parts[-1]
|
||||||
if help_url is None:
|
if help_url is None:
|
||||||
# no local help file is available, show on-line help
|
# no local help file is available, show on-line help
|
||||||
help_url = WEBSITE + 'manual.html'
|
help_url = utils.WEBSITE + 'manual.html'
|
||||||
# ask for localised manual
|
# ask for localised manual
|
||||||
if lang is not None:
|
if utils.lang is not None:
|
||||||
help_url += '?lang=' + lang
|
help_url += '?lang=' + utils.lang
|
||||||
# use a web browser to display the help documentation
|
# use a web browser to display the help documentation
|
||||||
webbrowser.open(help_url)
|
webbrowser.open(help_url)
|
||||||
|
|
||||||
|
@ -8307,31 +8193,70 @@ GObject.signal_new('reload', Diffuse.FileDiffViewer.PaneHeader, GObject.SignalFl
|
||||||
GObject.signal_new('save', Diffuse.FileDiffViewer.PaneHeader, GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ())
|
GObject.signal_new('save', Diffuse.FileDiffViewer.PaneHeader, GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ())
|
||||||
GObject.signal_new('save-as', Diffuse.FileDiffViewer.PaneHeader, GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ())
|
GObject.signal_new('save-as', Diffuse.FileDiffViewer.PaneHeader, GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ())
|
||||||
|
|
||||||
# create nested subdirectories and return the complete path
|
def main(version, sysconfigdir):
|
||||||
def make_subdirs(p, ss):
|
# app = Application()
|
||||||
for s in ss:
|
# return app.run(sys.argv)
|
||||||
p = os.path.join(p, s)
|
|
||||||
if not os.path.exists(p):
|
utils.VERSION = version
|
||||||
try:
|
|
||||||
os.mkdir(p)
|
args = sys.argv
|
||||||
except IOError:
|
argc = len(args)
|
||||||
pass
|
|
||||||
return p
|
if argc == 2 and args[1] in [ '-v', '--version' ]:
|
||||||
|
print('%s %s\n%s' % (utils.APP_NAME, utils.VERSION, utils.COPYRIGHT))
|
||||||
|
sys.exit(0)
|
||||||
|
if argc == 2 and args[1] in [ '-h', '-?', '--help' ]:
|
||||||
|
print(_('''Usage:
|
||||||
|
diffuse [ [OPTION...] [FILE...] ]...
|
||||||
|
diffuse ( -h | -? | --help | -v | --version )
|
||||||
|
|
||||||
|
Diffuse is a graphical tool for merging and comparing text files. Diffuse is
|
||||||
|
able to compare an arbitrary number of files side-by-side and gives users the
|
||||||
|
ability to manually adjust line matching and directly edit files. Diffuse can
|
||||||
|
also retrieve revisions of files from Bazaar, CVS, Darcs, Git, Mercurial,
|
||||||
|
Monotone, RCS, Subversion, and SVK repositories for comparison and merging.
|
||||||
|
|
||||||
|
Help Options:
|
||||||
|
( -h | -? | --help ) Display this usage information
|
||||||
|
( -v | --version ) Display version and copyright information
|
||||||
|
|
||||||
|
Configuration Options:
|
||||||
|
--no-rcfile Do not read any resource files
|
||||||
|
--rcfile <file> Specify explicit resource file
|
||||||
|
|
||||||
|
General Options:
|
||||||
|
( -c | --commit ) <rev> File revisions <rev-1> and <rev>
|
||||||
|
( -D | --close-if-same ) Close all tabs with no differences
|
||||||
|
( -e | --encoding ) <codec> Use <codec> to read and write files
|
||||||
|
( -L | --label ) <label> Display <label> instead of the file name
|
||||||
|
( -m | --modified ) Create a new tab for each modified file
|
||||||
|
( -r | --revision ) <rev> File revision <rev>
|
||||||
|
( -s | --separate ) Create a new tab for each file
|
||||||
|
( -t | --tab ) Start a new tab
|
||||||
|
( -V | --vcs ) <vcs-list> Version control system search order
|
||||||
|
--line <line> Start with line <line> selected
|
||||||
|
--null-file Create a blank file comparison pane
|
||||||
|
|
||||||
|
Display Options:
|
||||||
|
( -b | --ignore-space-change ) Ignore changes to white space
|
||||||
|
( -B | --ignore-blank-lines ) Ignore changes in blank lines
|
||||||
|
( -E | --ignore-end-of-line ) Ignore end of line differences
|
||||||
|
( -i | --ignore-case ) Ignore case differences
|
||||||
|
( -w | --ignore-all-space ) Ignore white space differences'''))
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
# process the command line arguments
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# find the config directory and create it if it didn't exist
|
# find the config directory and create it if it didn't exist
|
||||||
rc_dir, subdirs = os.environ.get('XDG_CONFIG_HOME', None), ['diffuse']
|
rc_dir, subdirs = os.environ.get('XDG_CONFIG_HOME', None), ['diffuse']
|
||||||
if rc_dir is None:
|
if rc_dir is None:
|
||||||
rc_dir = os.path.expanduser('~')
|
rc_dir = os.path.expanduser('~')
|
||||||
subdirs.insert(0, '.config')
|
subdirs.insert(0, '.config')
|
||||||
rc_dir = make_subdirs(rc_dir, subdirs)
|
rc_dir = utils.make_subdirs(rc_dir, subdirs)
|
||||||
# find the local data directory and create it if it didn't exist
|
# find the local data directory and create it if it didn't exist
|
||||||
data_dir, subdirs = os.environ.get('XDG_DATA_HOME', None), ['diffuse']
|
data_dir, subdirs = os.environ.get('XDG_DATA_HOME', None), ['diffuse']
|
||||||
if data_dir is None:
|
if data_dir is None:
|
||||||
data_dir = os.path.expanduser('~')
|
data_dir = os.path.expanduser('~')
|
||||||
subdirs[:0] = [ '.local', 'share' ]
|
subdirs[:0] = [ '.local', 'share' ]
|
||||||
data_dir = make_subdirs(data_dir, subdirs)
|
data_dir = utils.make_subdirs(data_dir, subdirs)
|
||||||
# load resource files
|
# load resource files
|
||||||
i, rc_files = 1, []
|
i, rc_files = 1, []
|
||||||
if i < argc and args[i] == '--no-rcfile':
|
if i < argc and args[i] == '--no-rcfile':
|
||||||
|
@ -8342,10 +8267,10 @@ if __name__ == '__main__':
|
||||||
i += 1
|
i += 1
|
||||||
else:
|
else:
|
||||||
# parse system wide then personal initialisation files
|
# parse system wide then personal initialisation files
|
||||||
if isWindows():
|
if utils.isWindows():
|
||||||
rc_file = os.path.join(bin_dir, 'diffuserc')
|
rc_file = os.path.join(utils.bin_dir, 'diffuserc')
|
||||||
else:
|
else:
|
||||||
rc_file = os.path.join(bin_dir, '@SYSCONFIGDIR@/diffuserc')
|
rc_file = os.path.join(utils.bin_dir, f'{sysconfigdir}/diffuserc')
|
||||||
for rc_file in rc_file, os.path.join(rc_dir, 'diffuserc'):
|
for rc_file in rc_file, os.path.join(rc_dir, 'diffuserc'):
|
||||||
if os.path.isfile(rc_file):
|
if os.path.isfile(rc_file):
|
||||||
rc_files.append(rc_file)
|
rc_files.append(rc_file)
|
||||||
|
@ -8354,11 +8279,14 @@ if __name__ == '__main__':
|
||||||
# reported with normalised file names
|
# reported with normalised file names
|
||||||
rc_file = os.path.abspath(rc_file)
|
rc_file = os.path.abspath(rc_file)
|
||||||
try:
|
try:
|
||||||
|
# diffuse.theResources.parse(rc_file) # Modularization
|
||||||
theResources.parse(rc_file)
|
theResources.parse(rc_file)
|
||||||
except IOError:
|
except IOError:
|
||||||
logError(_('Error reading %s.') % (rc_file, ))
|
utils.logError(_('Error reading %s.') % (rc_file, ))
|
||||||
|
|
||||||
|
# diff = diffuse.Diffuse(rc_dir) # Modularization
|
||||||
diff = Diffuse(rc_dir)
|
diff = Diffuse(rc_dir)
|
||||||
|
|
||||||
# load state
|
# load state
|
||||||
statepath = os.path.join(data_dir, 'state')
|
statepath = os.path.join(data_dir, 'state')
|
||||||
diff.loadState(statepath)
|
diff.loadState(statepath)
|
||||||
|
@ -8437,7 +8365,7 @@ if __name__ == '__main__':
|
||||||
try:
|
try:
|
||||||
options['line'] = int(args[i])
|
options['line'] = int(args[i])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logError(_('Error parsing line number.'))
|
utils.logError(_('Error parsing line number.'))
|
||||||
elif arg == '--null-file':
|
elif arg == '--null-file':
|
||||||
# add a blank file pane
|
# add a blank file pane
|
||||||
if mode == 'single' or mode == 'separate':
|
if mode == 'single' or mode == 'separate':
|
||||||
|
@ -8447,14 +8375,14 @@ if __name__ == '__main__':
|
||||||
revs = []
|
revs = []
|
||||||
had_specs = True
|
had_specs = True
|
||||||
else:
|
else:
|
||||||
logError(_('Skipping unknown argument "%s".') % (args[i], ))
|
utils.logError(_('Skipping unknown argument "%s".') % (args[i], ))
|
||||||
else:
|
else:
|
||||||
filename = diff.prefs.convertToNativePath(args[i])
|
filename = diff.prefs.convertToNativePath(args[i])
|
||||||
if (mode == 'single' or mode == 'separate') and os.path.isdir(filename):
|
if (mode == 'single' or mode == 'separate') and os.path.isdir(filename):
|
||||||
if len(specs) > 0:
|
if len(specs) > 0:
|
||||||
filename = os.path.join(filename, os.path.basename(specs[-1][0]))
|
filename = os.path.join(filename, os.path.basename(specs[-1][0]))
|
||||||
else:
|
else:
|
||||||
logError(_('Error processing argument "%s". Directory not expected.') % (args[i], ))
|
utils.logError(_('Error processing argument "%s". Directory not expected.') % (args[i], ))
|
||||||
filename = None
|
filename = None
|
||||||
if filename is not None:
|
if filename is not None:
|
||||||
if len(revs) == 0:
|
if len(revs) == 0:
|
||||||
|
@ -8483,3 +8411,5 @@ if __name__ == '__main__':
|
||||||
Gtk.main()
|
Gtk.main()
|
||||||
# save state
|
# save state
|
||||||
diff.saveState(statepath)
|
diff.saveState(statepath)
|
||||||
|
|
||||||
|
return 0
|
|
@ -1,42 +1,28 @@
|
||||||
# Diffuse binary file
|
pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name())
|
||||||
diffuse_conf = configuration_data()
|
moduledir = join_paths(pkgdatadir, meson.project_name())
|
||||||
diffuse_conf.set('SYSCONFIGDIR', sysconfdir)
|
sysconfdir = join_paths(get_option('prefix'), get_option('sysconfdir'))
|
||||||
|
|
||||||
|
python = import('python')
|
||||||
|
|
||||||
|
conf = configuration_data()
|
||||||
|
conf.set('PYTHON', python.find_installation('python3').path())
|
||||||
|
conf.set('VERSION', meson.project_version())
|
||||||
|
conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir')))
|
||||||
|
conf.set('pkgdatadir', pkgdatadir)
|
||||||
|
conf.set('sysconfigdir', sysconfdir)
|
||||||
|
|
||||||
configure_file(
|
configure_file(
|
||||||
input: 'usr/bin/diffuse.py.in',
|
input: 'diffuse.in',
|
||||||
output: 'diffuse',
|
output: 'diffuse',
|
||||||
configuration: diffuse_conf,
|
configuration: conf,
|
||||||
install: true,
|
install: true,
|
||||||
install_dir: bindir
|
install_dir: get_option('bindir')
|
||||||
)
|
)
|
||||||
|
|
||||||
# Diffuse config file
|
diffuse_sources = [
|
||||||
diffuserc_conf = configuration_data()
|
'__init__.py',
|
||||||
diffuserc_conf.set('PKGDATADIR', pkgdatadir)
|
'main.py',
|
||||||
|
'utils.py',
|
||||||
configure_file(
|
|
||||||
input: 'etc/diffuserc.py.in',
|
|
||||||
output: 'diffuserc',
|
|
||||||
configuration: diffuserc_conf,
|
|
||||||
install: true,
|
|
||||||
install_dir: sysconfdir
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate MetaInfo file
|
|
||||||
metainfo_file = join_paths(meson.source_root(), 'src/usr/share/metainfo/io.github.mightycreak.Diffuse.metainfo.xml')
|
|
||||||
ascli_exe = find_program('appstreamcli', required: false)
|
|
||||||
if ascli_exe.found()
|
|
||||||
test(
|
|
||||||
'validate metainfo file',
|
|
||||||
ascli_exe,
|
|
||||||
args: [
|
|
||||||
'validate',
|
|
||||||
'--no-net',
|
|
||||||
'--pedantic',
|
|
||||||
metainfo_file
|
|
||||||
]
|
]
|
||||||
)
|
|
||||||
endif
|
|
||||||
|
|
||||||
# Data files
|
install_data(diffuse_sources, install_dir: moduledir)
|
||||||
install_subdir('usr/share', install_dir: datadir, strip_directory: true)
|
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
# Diffuse: a graphical tool for merging and comparing text files.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2019 Derrick Moser <derrick_moser@yahoo.com>
|
||||||
|
# Copyright (C) 2021 Romain Failliot <romain.failliot@foolstep.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 2 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, write to the Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import locale
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
from gi.repository import Gtk
|
||||||
|
|
||||||
|
# convenience class for displaying a message dialogue
|
||||||
|
class MessageDialog(Gtk.MessageDialog):
|
||||||
|
def __init__(self, parent, type, s):
|
||||||
|
if type == Gtk.MessageType.ERROR:
|
||||||
|
buttons = Gtk.ButtonsType.OK
|
||||||
|
else:
|
||||||
|
buttons = Gtk.ButtonsType.OK_CANCEL
|
||||||
|
Gtk.MessageDialog.__init__(self, parent = parent, destroy_with_parent = True, message_type = type, buttons = buttons, text = s)
|
||||||
|
self.set_title(APP_NAME)
|
||||||
|
|
||||||
|
# platform test
|
||||||
|
def isWindows():
|
||||||
|
return os.name == 'nt'
|
||||||
|
|
||||||
|
# convenience function to display debug messages
|
||||||
|
def logDebug(s):
|
||||||
|
pass #sys.stderr.write(f'{APP_NAME}: {s}\n')
|
||||||
|
|
||||||
|
# report error messages
|
||||||
|
def logError(s):
|
||||||
|
m = MessageDialog(None, Gtk.MessageType.ERROR, s)
|
||||||
|
m.run()
|
||||||
|
m.destroy()
|
||||||
|
|
||||||
|
# create nested subdirectories and return the complete path
|
||||||
|
def make_subdirs(p, ss):
|
||||||
|
for s in ss:
|
||||||
|
p = os.path.join(p, s)
|
||||||
|
if not os.path.exists(p):
|
||||||
|
try:
|
||||||
|
os.mkdir(p)
|
||||||
|
except IOError:
|
||||||
|
pass
|
||||||
|
return p
|
||||||
|
|
||||||
|
# use the program's location as a starting place to search for supporting files
|
||||||
|
# such as icon and help documentation
|
||||||
|
if hasattr(sys, 'frozen'):
|
||||||
|
app_path = sys.executable
|
||||||
|
else:
|
||||||
|
app_path = os.path.realpath(sys.argv[0])
|
||||||
|
bin_dir = os.path.dirname(app_path)
|
||||||
|
|
||||||
|
# translation location: '../share/locale/<LANG>/LC_MESSAGES/diffuse.mo'
|
||||||
|
# where '<LANG>' is the language key
|
||||||
|
lang = locale.getdefaultlocale()[0]
|
||||||
|
if isWindows():
|
||||||
|
# gettext looks for the language using environment variables which
|
||||||
|
# are normally not set on Windows so we try setting it for them
|
||||||
|
for v in 'LC_ALL', 'LC_CTYPE', 'LANG', 'LANGUAGE':
|
||||||
|
if v in os.environ:
|
||||||
|
lang = os.environ[v]
|
||||||
|
# remove any additional languages, encodings, or modifications
|
||||||
|
for v in ':.@':
|
||||||
|
lang = lang.split(v)[0]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if lang is not None:
|
||||||
|
os.environ['LANG'] = lang
|
||||||
|
|
||||||
|
APP_NAME = 'Diffuse'
|
||||||
|
VERSION = '0.0.0'
|
||||||
|
COPYRIGHT = '''{copyright} © 2006-2019 Derrick Moser
|
||||||
|
{copyright} © 2015-2021 Romain Failliot'''.format(copyright=_("Copyright"))
|
||||||
|
WEBSITE = 'https://mightycreak.github.io/diffuse/'
|
|
@ -197,7 +197,7 @@ for lang in os.listdir(d):
|
||||||
j = s.find(';', i)
|
j = s.find(';', i)
|
||||||
a.append(unichr(int(s[i:j])))
|
a.append(unichr(int(s[i:j])))
|
||||||
idx = j + 1
|
idx = j + 1
|
||||||
s = u''.join(a)
|
s = ''.join(a)
|
||||||
s = codecs.encode(s, 'utf-8')
|
s = codecs.encode(s, 'utf-8')
|
||||||
# clean up translator credit portion
|
# clean up translator credit portion
|
||||||
div = extract_tag(s, '<div class="othercredit">', '</div>')
|
div = extract_tag(s, '<div class="othercredit">', '</div>')
|
||||||
|
|
Loading…
Reference in New Issue